Go: proposal: spesifikasi: fasilitas pemrograman generik

Dibuat pada 14 Apr 2016  ·  816Komentar  ·  Sumber: golang/go

Masalah ini mengusulkan bahwa Go harus mendukung beberapa bentuk pemrograman generik.
Ini memiliki label Go2, karena untuk Go1.x bahasanya kurang lebih sudah selesai.

Mendampingi masalah ini adalah proposal generik umum oleh @ianlancetaylor yang mencakup empat proposal cacat spesifik dari mekanisme pemrograman generik untuk Go.

Tujuannya bukan untuk menambahkan generik ke Go saat ini, melainkan untuk menunjukkan kepada orang-orang seperti apa proposal yang lengkap. Kami berharap ini akan membantu siapa pun yang mengusulkan perubahan bahasa serupa di masa mendatang.

Go2 LanguageChange NeedsInvestigation Proposal generics

Komentar yang paling membantu

Izinkan saya terlebih dahulu mengingatkan semua orang tentang kebijakan https://golang.org/wiki/NoMeToo kami. Pesta emoji ada di atas.

Semua 816 komentar

CL https://golang.org/cl/22057 menyebutkan masalah ini.

Izinkan saya terlebih dahulu mengingatkan semua orang tentang kebijakan https://golang.org/wiki/NoMeToo kami. Pesta emoji ada di atas.

Ada Summary of Go Generics Discussions , yang mencoba memberikan gambaran umum tentang diskusi dari berbagai tempat. Ini juga memberikan beberapa contoh bagaimana memecahkan masalah, di mana Anda ingin menggunakan obat generik.

Ada dua "persyaratan" dalam proposal tertaut yang dapat memperumit implementasi dan mengurangi keamanan jenis:

  • Definisikan tipe generik berdasarkan tipe yang tidak diketahui sampai mereka dipakai.
  • Tidak memerlukan hubungan eksplisit antara definisi tipe atau fungsi generik dan penggunaannya. Artinya, program tidak harus secara eksplisit mengatakan tipe T mengimplementasikan G generik.

Persyaratan ini tampaknya mengecualikan misalnya sistem yang mirip dengan sistem sifat Rust, di mana tipe generik dibatasi oleh batas sifat. Mengapa ini dibutuhkan?

Menjadi tergoda untuk membangun obat generik ke perpustakaan standar pada tingkat yang sangat rendah, seperti dalam C++ std::basic_string, std::pengalokasi>. Ini memiliki keuntungan—jika tidak, tidak ada yang akan melakukannya—tetapi ini memiliki efek yang luas dan terkadang mengejutkan, seperti pada pesan kesalahan C++ yang tidak dapat dipahami.

Masalah dalam C++ muncul dari pengecekan tipe kode yang dihasilkan. Perlu ada pemeriksaan tipe tambahan sebelum pembuatan kode. Proposal konsep C++ memungkinkan ini dengan mengizinkan pembuat kode generik untuk menentukan persyaratan tipe generik. Dengan begitu, kompilasi dapat gagal memeriksa jenis sebelum pembuatan kode dan pesan kesalahan sederhana dapat dicetak. Masalah dengan C++ generik (tanpa konsep) adalah bahwa kode generik _adalah_ spesifikasi tipe generik. Itulah yang menciptakan pesan kesalahan yang tidak dapat dipahami.

Kode generik tidak boleh menjadi spesifikasi tipe generik.

@tamird Ini adalah fitur penting dari tipe antarmuka Go sehingga Anda dapat mendefinisikan tipe non-antarmuka T dan kemudian menentukan tipe antarmuka I sehingga T mengimplementasikan I. Lihat https://golang.org/doc/faq#implements_interface . Akan menjadi tidak konsisten jika Go mengimplementasikan suatu bentuk generik di mana tipe generik G hanya dapat digunakan dengan tipe T yang secara eksplisit mengatakan "Saya dapat digunakan untuk mengimplementasikan G."

Saya tidak akrab dengan Rust, tetapi saya tidak tahu bahasa apa pun yang mengharuskan T untuk secara eksplisit menyatakan bahwa itu dapat digunakan untuk mengimplementasikan G. Kedua persyaratan yang Anda sebutkan tidak berarti bahwa G tidak dapat memaksakan persyaratan pada T, hanya karena saya memaksakan persyaratan pada T. Persyaratan hanya berarti bahwa G dan T dapat ditulis secara independen. Itu adalah fitur yang sangat diinginkan untuk obat generik, dan saya tidak bisa membayangkan meninggalkannya.

@ianlancetaylor https://doc.rust-lang.org/book/traits.html menjelaskan sifat Rust. Sementara saya pikir mereka adalah model yang baik secara umum, mereka tidak cocok untuk Go seperti yang ada saat ini.

@sbunce Saya juga berpikir bahwa konsep adalah jawabannya, dan Anda dapat melihat ide tersebar melalui berbagai proposal sebelum yang terakhir. Tetapi mengecewakan bahwa konsep awalnya direncanakan untuk apa yang menjadi C++ 11, dan sekarang tahun 2016, dan mereka masih kontroversial dan tidak terlalu dekat untuk dimasukkan dalam bahasa C++.

Akankah ada nilai pada literatur akademis untuk panduan dalam mengevaluasi pendekatan?

Satu-satunya makalah yang saya baca tentang topik ini adalah Apakah pengembang mendapat manfaat dari tipe generik? (maaf paywall, Anda mungkin mencari cara untuk mengunduh pdf di Google) yang memiliki yang berikut untuk dikatakan

Akibatnya, interpretasi konservatif dari eksperimen
adalah bahwa tipe generik dapat dianggap sebagai tradeoff
antara karakteristik dokumentasi positif dan
karakteristik ekstensibilitas negatif. Bagian yang menarik dari
penelitian ini menunjukkan situasi di mana penggunaan a
(lebih kuat) sistem tipe statis memiliki dampak negatif pada
waktu pengembangan sementara pada saat yang sama manfaat yang diharapkan
fit – pengurangan waktu perbaikan kesalahan tipe – tidak muncul.
Kami berpikir bahwa tugas-tugas tersebut dapat membantu dalam eksperimen masa depan di
mengidentifikasi dampak dari sistem tipe.

Saya juga melihat https://github.com/golang/go/issues/15295 juga mereferensikan obat generik berorientasi objek yang ringan dan fleksibel .

Jika kita akan bersandar pada akademisi untuk memandu keputusan, saya pikir akan lebih baik untuk melakukan tinjauan literatur di awal, dan mungkin memutuskan lebih awal jika kita akan menimbang studi empiris secara berbeda dari yang mengandalkan bukti.

Silakan lihat: http://dl.acm.org/citation.cfm?id=2738008 oleh Barbara Liskov:

Dukungan untuk pemrograman generik dalam bahasa pemrograman berorientasi objek modern canggung dan tidak memiliki kekuatan ekspresif yang diinginkan. Kami memperkenalkan mekanisme generik ekspresif yang menambahkan kekuatan ekspresif dan memperkuat pemeriksaan statis, sambil tetap ringan dan sederhana dalam kasus penggunaan umum. Seperti kelas tipe dan konsep, mekanisme memungkinkan tipe yang ada untuk memodelkan batasan tipe secara retroaktif. Untuk kekuatan ekspresif, kami mengekspos model sebagai konstruksi bernama yang dapat didefinisikan dan dipilih secara eksplisit untuk menyaksikan kendala; dalam penggunaan umum dari kedermawanan, bagaimanapun, tipe secara implisit menyaksikan kendala tanpa upaya programmer tambahan.

Saya pikir apa yang mereka lakukan di sana cukup keren - saya minta maaf jika ini adalah tempat yang salah untuk berhenti tetapi saya tidak dapat menemukan tempat untuk berkomentar di /proposals dan saya tidak menemukan masalah yang sesuai di sini.

Mungkin menarik untuk memiliki satu atau lebih transpiler eksperimental - kode sumber generik Go ke kompiler kode sumber Go 1.xy.
Maksud saya - terlalu banyak bicara/argumen-untuk-pendapat saya, dan tidak ada yang menulis kode sumber yang _coba_ untuk mengimplementasikan _beberapa jenis_ obat generik untuk Go.

Hanya untuk mendapatkan pengetahuan dan pengalaman dengan Go dan obat generik - untuk melihat apa yang berhasil dan apa yang tidak.
Jika semua solusi generik Go tidak terlalu bagus, maka; Tidak ada obat generik untuk Go.

Bisakah proposal juga menyertakan implikasi pada ukuran biner dan jejak memori? Saya berharap akan ada duplikasi kode untuk setiap jenis nilai konkret sehingga pengoptimalan kompiler bekerja pada mereka. Saya berharap ada jaminan bahwa tidak akan ada duplikasi kode untuk tipe pointer beton.

Saya menawarkan matriks Keputusan Pugh. Kriteria saya termasuk dampak persepsi (kompleksitas sumber, ukuran). Saya juga memaksa peringkat kriteria untuk menentukan bobot kriteria. Anda sendiri dapat bervariasi tentu saja. Saya menggunakan "antarmuka" sebagai alternatif default dan membandingkannya dengan obat generik "salin/tempel", obat generik berbasis template (saya memikirkan sesuatu seperti cara kerja bahasa D), dan sesuatu yang saya sebut generik gaya instantiasi runtime. Saya yakin ini adalah penyederhanaan yang berlebihan. Meskipun demikian, ini mungkin memicu beberapa ide tentang cara mengevaluasi pilihan... ini harus menjadi tautan publik ke Google Sheet saya, di sini

Ping ke @yizhouzhang dan @andrewcmyers sehingga mereka dapat menyuarakan pendapat mereka tentang genus seperti obat generik di Go. Kedengarannya seperti itu bisa menjadi pasangan yang baik :)

Desain generik yang kami buat untuk Genus memiliki pemeriksaan tipe modular yang statis, tidak memerlukan deklarasi sebelumnya bahwa tipe mengimplementasikan beberapa antarmuka, dan dilengkapi dengan kinerja yang wajar. Saya pasti akan melihatnya jika Anda berpikir tentang obat generik untuk Go. Sepertinya cocok dari pemahaman saya tentang Go.

Berikut ini tautan ke makalah yang tidak memerlukan akses Perpustakaan Digital ACM:
http://www.cs.cornell.edu/andru/papers/genus/

Halaman beranda Genus ada di sini: http://www.cs.cornell.edu/projects/genus/

Kami belum merilis kompiler secara publik, tetapi kami berencana untuk melakukannya segera.

Senang menjawab pertanyaan apa pun yang dimiliki orang.

Dalam hal matriks keputusan @mandolyte , Genus mendapat skor 17, terikat untuk #1. Saya akan menambahkan beberapa kriteria lagi untuk mencetak gol. Misalnya, pemeriksaan tipe modular penting, seperti yang lainnya seperti @sbunce yang diamati di atas, tetapi skema berbasis template tidak memilikinya. Laporan teknis untuk makalah Genus memiliki tabel yang jauh lebih besar di halaman 34, membandingkan berbagai desain generik.

Saya baru saja membaca seluruh dokumen Ringkasan Go Generics , yang merupakan ringkasan bermanfaat dari diskusi sebelumnya. Mekanisme generik di Genus, menurut saya, tidak mengalami masalah yang diidentifikasi untuk C++, Java, atau C#. Genus generik direifikasi, tidak seperti di Java, sehingga Anda dapat mengetahui jenisnya saat run time. Anda juga dapat membuat instance pada tipe primitif, dan Anda tidak mendapatkan tinju implisit di tempat yang sebenarnya tidak Anda inginkan: array T di mana T adalah primitif. Sistem tipenya paling dekat dengan Haskell dan Rust -- sebenarnya sedikit lebih kuat, tapi menurut saya juga intuitif. Spesialisasi primitif ala C# saat ini tidak didukung di Genus tetapi bisa jadi. Dalam kebanyakan kasus, spesialisasi dapat ditentukan pada waktu tautan, sehingga pembuatan kode run-time yang sebenarnya tidak diperlukan.

CL https://golang.org/cl/22163 menyebutkan masalah ini.

Cara untuk membatasi tipe generik yang tidak memerlukan penambahan konsep bahasa baru: https://docs.google.com/document/d/1rX4huWffJ0y1ZjqEpPrDy-kk-m9zWfatgCluGRBQveQ/edit?usp=sharing.

Genus terlihat sangat keren dan jelas merupakan kemajuan seni yang penting, tetapi saya tidak melihat bagaimana hal itu akan diterapkan pada Go. Adakah yang punya sketsa bagaimana itu akan diintegrasikan dengan sistem/filosofi tipe Go?

Masalahnya adalah tim go adalah upaya penghalang. Judulnya dengan jelas menyatakan niat tim go. Dan jika itu tidak cukup untuk menghalangi semua pengambil, fitur yang dituntut dari domain yang begitu luas dalam proposal oleh ian memperjelas bahwa jika Anda menginginkan obat generik maka mereka tidak menginginkan Anda. Tidak masuk akal bahkan mencoba berdialog dengan tim go. Bagi mereka yang mencari obat generik, saya katakan patahkan bahasanya. Mulailah perjalanan baru - banyak yang akan mengikuti. Saya telah melihat beberapa pekerjaan hebat yang dilakukan di garpu. Atur diri Anda, bersatu untuk suatu tujuan

Jika ada yang ingin mencoba membuat ekstensi generik untuk Go berdasarkan desain Genus, kami dengan senang hati membantu. Kami tidak tahu Go cukup baik untuk menghasilkan desain yang selaras dengan bahasa yang ada. Saya pikir langkah pertama adalah proposal desain manusia jerami dengan contoh-contoh yang berhasil.

@andrewcmyers berharap @ianlancetaylor akan bekerja dengan Anda dalam hal itu. Hanya memiliki beberapa contoh untuk dilihat akan banyak membantu.

Saya telah membaca makalah Genus. Sejauh yang saya pahami, sepertinya bagus untuk Java, tapi sepertinya tidak cocok untuk Go.

Salah satu aspek kunci dari Go adalah ketika Anda menulis program Go, sebagian besar yang Anda tulis adalah kode. Ini berbeda dari C++ dan Java, di mana lebih banyak dari apa yang Anda tulis adalah tipe. Genus tampaknya sebagian besar tentang tipe: Anda menulis batasan dan model, bukan kode. Sistem tipe Go sangat sangat sederhana. Sistem tipe Genus jauh lebih kompleks.

Ide pemodelan retroaktif, meskipun jelas berguna untuk Java, tampaknya tidak cocok dengan Go sama sekali. Orang sudah menggunakan tipe adaptor untuk mencocokkan tipe yang ada dengan antarmuka; tidak ada lagi yang diperlukan saat menggunakan obat generik.

Akan menarik untuk melihat ide-ide ini diterapkan pada Go, tetapi saya tidak optimis dengan hasilnya.

Saya bukan ahli Go, tetapi sistem tipenya tampaknya tidak lebih sederhana daripada Java pra-generik. Sintaks tipenya sedikit lebih ringan dengan cara yang bagus tetapi kompleksitas yang mendasarinya tampaknya hampir sama.

Di Genus, batasan adalah tipe tetapi model adalah kode. Model adalah adaptor, tetapi mereka beradaptasi tanpa menambahkan lapisan pembungkus yang sebenarnya. Ini sangat berguna ketika Anda ingin, katakanlah, mengadaptasi seluruh array objek ke antarmuka baru. Pemodelan retroaktif memungkinkan Anda memperlakukan larik sebagai larik objek yang memenuhi antarmuka yang diinginkan.

Saya tidak akan terkejut jika itu lebih rumit daripada Java (pra-generik) dalam pengertian teoretis tipe, meskipun lebih sederhana untuk digunakan dalam praktik.

Selain kompleksitas relatif, mereka cukup berbeda sehingga Genus tidak dapat memetakan 1:1. Tidak ada subtipe yang sepertinya besar.

Jika Anda tertarik:

Ringkasan singkat dari perbedaan filosofis/desain relevan yang saya sebutkan terdapat dalam entri FAQ berikut:

Tidak seperti kebanyakan bahasa, spesifikasi Go sangat singkat dan jelas tentang properti yang relevan dari sistem tipe mulai dari https://golang.org/ref/spec#Constants dan terus berlanjut hingga bagian berjudul "Blocks" (semuanya dicetak kurang dari 11 halaman).

Tidak seperti generik Java dan C#, mekanisme generik Genus tidak didasarkan pada subtipe. Di sisi lain, menurut saya Go memang memiliki subtipe, tetapi subtipe struktural. Itu juga cocok untuk pendekatan Genus, yang memiliki cita rasa struktural daripada mengandalkan hubungan yang telah dideklarasikan sebelumnya.

Saya tidak percaya bahwa Go memiliki subtipe struktural.

Sementara dua tipe yang tipe dasarnya identik karena itu identik
bisa saling menggantikan, https://play.golang.org/p/cT15aQ-PFr

Ini tidak mencakup dua jenis yang berbagi subset bidang yang sama,
https://play.golang.org/p/KrC9_BDXuh.

Pada Kam, 28 Apr 2016 jam 13:09, Andrew [email protected]
menulis:

Tidak seperti generik Java dan C#, mekanisme generik Genus tidak didasarkan pada
subtipe. Di sisi lain, menurut saya Go memang memiliki subtipe,
tetapi subtipe struktural. Itu juga cocok untuk pendekatan Genus,
yang memiliki cita rasa struktural daripada mengandalkan yang telah dinyatakan sebelumnya
hubungan.


Anda menerima ini karena Anda berlangganan utas ini.
Balas email ini secara langsung atau lihat di GitHub
https://github.com/golang/go/issues/15292#issuecomment -215298127

Terima kasih, saya salah mengartikan beberapa bahasa tentang kapan tipe mengimplementasikan antarmuka. Sebenarnya, menurut saya antarmuka Go, dengan ekstensi sederhana, dapat digunakan sebagai batasan gaya Genus.

Itulah mengapa saya melakukan ping kepada Anda, genus sepertinya merupakan pendekatan yang jauh lebih baik daripada Java/C# seperti obat generik.

Ada beberapa ide yang berkaitan dengan spesialisasi pada tipe antarmuka; misalnya _package templates_ pendekatan "proposal" 1 2 adalah contohnya.

tl; dr; paket generik dengan spesialisasi antarmuka akan terlihat seperti:

package set
type E interface { Equal(other E) bool }
type Set struct { items []E }
func (s *Set) Add(item E) { ... }

Versi 1. dengan spesialisasi cakupan paket:

package main
import items set[[E: *Item]]

type Item struct { ... }
func (a *Item) Equal(b *Item) bool { ... }

var xs items.Set
xs.Add(&Item{})

Versi 2. spesialisasi cakupan deklarasi:

package main
import set

type Item struct { ... }
func (a *Item) Equal(b *Item) bool { ... }

var xs set.Set[[E: *Item]]
xs.Add(&Item{})

Obat generik yang dicakup paket akan mencegah orang menyalahgunakan sistem generik secara signifikan, karena penggunaannya terbatas pada algoritme dasar dan struktur data. Ini pada dasarnya mencegah membangun abstraksi bahasa dan kode fungsional baru.

Spesialisasi cakupan deklarasi memiliki lebih banyak kemungkinan dengan biaya sehingga lebih rentan terhadap penyalahgunaan dan lebih bertele-tele. Tetapi, kode fungsional dimungkinkan, misalnya:

type E interface{}
func Reduce(zero E, items []E, fn func(a, b E) E) E { ... }

Reduce[[E: int]](0, []int{1,2,3,4}, func(a, b int)int { return a + b } )
// there are probably ways to have some aliases (or similar) to make it less verbose
alias ReduceInt Reduce[[E: int]]
func ReduceInt Reduce[[E: int]]

Pendekatan spesialisasi antarmuka memiliki properti yang menarik:

  • Paket yang sudah ada menggunakan antarmuka akan dapat dispesialisasikan. misalnya saya akan dapat memanggil sort.Sort[[Interface:MyItems]](...) dan memiliki pekerjaan penyortiran pada tipe beton bukan antarmuka (dengan potensi keuntungan dari inlining).
  • Pengujian disederhanakan, saya hanya perlu memastikan bahwa kode generik berfungsi dengan antarmuka.
  • Sangat mudah untuk menyatakan cara kerjanya. yaitu bayangkan [[E: int]] mengganti semua deklarasi E dengan int .

Tapi, ada masalah verbositas saat bekerja di seluruh paket:

type op
import "set"

type E interface{}
func Union(a, b set.Set[[set.E: E]]) set.Set[[set.E: E]] {
    result := set.New[[set.E: E]]()
    ...
}

_Tentu saja, semuanya lebih sederhana untuk dinyatakan daripada diimplementasikan. Secara internal mungkin ada banyak masalah dan cara bagaimana itu bisa bekerja._

_PS, untuk para gerutuan tentang kemajuan obat generik yang lambat, saya memuji Tim Go karena menghabiskan lebih banyak waktu untuk masalah yang memiliki manfaat lebih besar bagi komunitas, misalnya bug kompiler/runtime, SSA, GC, http2._

@egonelbre poin Anda bahwa obat generik tingkat paket akan mencegah "penyalahgunaan" adalah hal yang sangat penting yang menurut saya diabaikan oleh kebanyakan orang. Itu ditambah kesederhanaan semantik dan sintaksis relatif mereka (hanya paket dan konstruksi impor yang terpengaruh) membuatnya sangat menarik untuk Go.

@andrewcymyers menarik bahwa menurut Anda antarmuka Go berfungsi sebagai batasan gaya Genus. Saya akan berpikir mereka masih memiliki masalah bahwa Anda tidak dapat mengungkapkan batasan parameter multi-tipe dengan mereka.

Namun, satu hal yang baru saya sadari adalah bahwa di Go Anda dapat menulis antarmuka sebaris. Jadi dengan sintaks yang tepat, Anda dapat menempatkan antarmuka dalam cakupan semua parameter dan menangkap batasan multi-parameter:

ketik [V, E] Grafik [V antarmuka { Tepi() E }, E antarmuka { Titik akhir() (V, V) }] ...

Saya pikir masalah yang lebih besar dengan antarmuka sebagai kendala adalah bahwa metode tidak meresap di Go seperti di Jawa. Tipe bawaan tidak memiliki metode. Tidak ada kumpulan metode universal seperti yang ada di java.lang.Object. Pengguna biasanya tidak mendefinisikan metode seperti Equals atau HashCode pada tipe mereka kecuali mereka secara khusus membutuhkannya, karena metode tersebut tidak memenuhi syarat untuk digunakan sebagai kunci peta, atau dalam algoritme apa pun yang membutuhkan kesetaraan.

(Kesetaraan di Go adalah cerita yang menarik. Bahasa memberi jenis Anda "==" jika memenuhi persyaratan tertentu (lihat https://golang.org/ref/spec#Logical_operators, cari "sebanding"). Jenis apa pun dengan " =" dapat berfungsi sebagai kunci peta. Tetapi jika jenis Anda tidak layak mendapatkan "==", maka tidak ada yang dapat Anda tulis yang akan membuatnya berfungsi sebagai kunci peta.)

Karena metode tidak meresap, dan karena tidak ada cara mudah untuk mengekspresikan properti dari tipe bawaan (seperti operator yang mereka gunakan), saya menyarankan untuk menggunakan kode itu sendiri sebagai mekanisme batasan umum. Lihat tautan di komentar saya tanggal 18 April di atas. Proposal ini memiliki masalah, tetapi satu fitur yang bagus adalah bahwa kode numerik generik masih dapat menggunakan operator biasa, alih-alih pemanggilan metode yang rumit.

Cara lain adalah menambahkan metode ke tipe yang tidak memilikinya. Anda dapat melakukan ini dalam bahasa yang ada dengan cara yang jauh lebih ringan daripada di Jawa:

ketik Int ke
func (i Int) Lebih sedikit(j Int) bool { kembali i < j }

Jenis Int "mewarisi" semua operator dan properti int lainnya. Meskipun Anda harus melakukan cast di antara keduanya untuk menggunakan Int dan int bersama-sama, yang bisa jadi menyusahkan.

Model genus dapat membantu di sini. Tetapi mereka harus dibuat sangat sederhana. Saya pikir @ianlancetaylor terlalu sempit dalam mengkarakterisasi Go karena menulis lebih banyak kode, lebih sedikit tipe. Prinsip umum adalah bahwa Go membenci kompleksitas. Kami melihat Java dan C++ dan bertekad untuk tidak pernah pergi ke sana. (Tanpa bermaksud menyinggung.)

Jadi satu ide cepat untuk fitur seperti model adalah: minta pengguna menulis tipe seperti Int di atas, dan dalam instantiasi umum izinkan "int dengan Int", artinya gunakan tipe int tetapi perlakukan seperti Int. Kemudian tidak ada konstruksi bahasa terbuka yang disebut model, dengan kata kuncinya, semantik pewarisan, dan sebagainya. Saya tidak memahami model dengan cukup baik untuk mengetahui apakah ini layak, tetapi ini lebih dalam semangat Go.

@jba Kami tentu setuju dengan prinsip menghindari kompleksitas. "Sesederhana mungkin tapi tidak sesederhana itu." Saya mungkin akan meninggalkan beberapa fitur Genus dari Go dengan alasan itu, setidaknya pada awalnya.

Salah satu hal bagus tentang pendekatan Genus adalah ia menangani tipe bawaan dengan lancar. Ingat bahwa tipe primitif di Java tidak memiliki metode, dan Genus mewarisi perilaku ini. Sebaliknya, Genus memperlakukan tipe primitif _seolah-olah_ mereka memiliki rangkaian metode yang cukup besar untuk tujuan memenuhi batasan. Tabel hash mensyaratkan bahwa kuncinya dapat di-hash dan dibandingkan, tetapi semua tipe primitif memenuhi batasan ini. Jadi, ketik instantiasi seperti Map[int, boolean] benar-benar legal tanpa perlu repot lagi. Tidak perlu membedakan antara dua rasa bilangan bulat (int vs Int) untuk mencapai ini. Namun, jika int tidak dilengkapi dengan operasi yang cukup untuk beberapa penggunaan, kami akan menggunakan model yang hampir persis seperti penggunaan Int di atas.

Hal lain yang layak disebutkan adalah gagasan "model alami" di Genus. Anda biasanya tidak perlu mendeklarasikan model untuk menggunakan tipe generik: jika argumen tipe memenuhi batasan, model alami dibuat secara otomatis. Pengalaman kami adalah bahwa ini adalah kasus yang biasa; menyatakan eksplisit, model bernama biasanya tidak diperlukan. Tetapi jika model diperlukan — misalnya, jika Anda ingin hash int dengan cara yang tidak standar — maka sintaksnya mirip dengan yang Anda sarankan: Map[int with fancyHash, boolean] . Saya berpendapat bahwa Genus secara sintaksis ringan dalam kasus penggunaan normal tetapi dengan daya cadangan saat diperlukan.

@egonelbre Apa yang Anda usulkan di sini terlihat seperti tipe virtual, yang didukung oleh Scala. Ada makalah ECOOP'97 oleh Kresten Krab Thorup, "Genericity in Java with virtual types", yang mengeksplorasi arah ini. Kami juga mengembangkan mekanisme untuk tipe virtual dan kelas virtual dalam pekerjaan kami ("J&: persimpangan bersarang untuk komposisi perangkat lunak yang dapat diskalakan", OOPSLA'06).

Karena inisialisasi literal meresap di Go, saya harus bertanya-tanya seperti apa fungsi literal itu. Saya menduga bahwa kode untuk menangani ini sebagian besar ada di Go generate, fix, dan rename.Mungkin itu akan menginspirasi seseorang :-)

// definisi tipe func (generik)
ketik Sum64 func (X, Y) float64 {
kembali float64(X) + float64(Y)
}

// instantiate satu, secara posisi
saya := 42
var ju uint = 86
jumlah := &Jumlah64{i, j}

// instantiate satu, dengan tipe parameter bernama
jumlah := &Jumlah64{ X: int, Y: uint}

// sekarang gunakan...
hasil := jumlah(i, j) // hasil 128

Usulan Ian menuntut terlalu banyak. Kami tidak mungkin mengembangkan semua fitur sekaligus, itu akan ada dalam keadaan yang belum selesai selama berbulan-bulan.

Sementara itu, proyek yang belum selesai tidak dapat disebut bahasa Go resmi sampai selesai karena akan berisiko memecah ekosistem.

Jadi pertanyaannya adalah bagaimana merencanakan ini.

Juga sebagian besar proyek akan mengembangkan korpus referensi.
mengembangkan koleksi generik yang sebenarnya, algoritme, dan hal-hal lain sedemikian rupa sehingga kita semua sepakat bahwa mereka idiomatik, saat menggunakan fitur go 2.0 yang baru

Sintaks yang mungkin?

// Module defining generic type
module list(type t)

type node struct {
    next *node
    data t
}
// Module using generic type:
import (
    intlist "module/path/to/list" (int)
    funclist "module/path/to/list" (func (int) int)
)

l := intlist.New()
l.Insert(5)

@md2perpe , sintaks bukanlah bagian yang sulit dari masalah ini. Bahkan, sejauh ini yang paling mudah. Silakan lihat diskusi dan dokumen terkait di atas.

@ md2perpe Kami telah membahas parametrizing seluruh paket ("modul") sebagai cara untuk generik secara internal - ini tampaknya menjadi cara untuk mengurangi overhead sintaksis. Tetapi memiliki masalah lain; misalnya, tidak jelas bagaimana seseorang akan mengukurnya dengan tipe yang bukan level paket. Tapi idenya mungkin masih layak untuk ditelusuri secara detail.

Saya ingin berbagi perspektif: Di alam semesta paralel, semua tanda tangan fungsi Go selalu dibatasi untuk hanya menyebutkan jenis antarmuka, dan alih-alih permintaan untuk obat generik hari ini, ada satu cara untuk menghindari tipuan yang terkait dengan nilai antarmuka. Pikirkan bagaimana Anda akan memecahkan masalah itu (tanpa mengubah bahasa). Saya punya beberapa ide.

@thwd Jadi, penulis perpustakaan akan terus menggunakan antarmuka, tetapi tanpa pengalihan jenis dan jenis pernyataan yang diperlukan saat ini. Dan akankah pengguna perpustakaan hanya meneruskan tipe konkret seolah-olah perpustakaan akan menggunakan tipe apa adanya... dan kemudian apakah kompiler akan mendamaikan keduanya? Dan jika itu tidak bisa menyatakan mengapa? (seperti operator modulo digunakan di perpustakaan, tetapi pengguna menyediakan sepotong sesuatu.

Apakah saya dekat? :-)

@mandolyte ya! mari bertukar email agar tidak mencemari thread ini. Anda dapat menghubungi saya di "saya di thwd dot me". Siapa pun yang membaca ini yang mungkin tertarik; kirimi saya email dan saya akan menambahkan Anda ke utas.

Ini fitur hebat untuk type system dan collection library .
Sintaks potensial:

type Element<T> struct {
    prev, next *Element<T>
    list *List<T>
    value T
}
type List<E> struct {
    root Element<E>
    len int
}

Untuk interface

type Collection<E> interface {
    Size() int
    Add(e E) bool
}

super type atau type implement :

func contain(l List<parent E>, e E) bool
<V> func (c Collection<child E>)Map(fn func(e E) V) Collection

Di atas alias di Jawa:

boolean contain(List<? super E>, E e)
<V> Collection Map(Function<? extend E, V> mapFunc);

@leaxoy seperti yang dikatakan sebelumnya, sintaks bukanlah bagian yang sulit di sini. Lihat pembahasan di atas.

Perlu diketahui bahwa biaya antarmuka luar biasa besar.

Tolong jelaskan mengapa menurut Anda biaya antarmuka "luar biasa"
besar.
Seharusnya tidak lebih buruk daripada panggilan virtual non-khusus C++.

@minux Saya tidak bisa mengatakan tentang biaya kinerja tetapi dalam kaitannya dengan kualitas kode. interface{} tidak dapat diverifikasi pada waktu kompilasi tetapi obat generik bisa. Menurut pendapat saya ini, dalam banyak kasus, lebih penting daripada masalah kinerja menggunakan interface{} .

@xoviat

Sebenarnya tidak ada kerugian untuk ini karena pemrosesan yang diperlukan untuk ini tidak memperlambat kompiler.

Ada (setidaknya) dua kelemahan.

Salah satunya adalah peningkatan pekerjaan untuk linker: jika spesialisasi untuk dua jenis menghasilkan kode mesin dasar yang sama, kami tidak ingin mengompilasi dan menautkan dua salinan kode itu.

Lain adalah bahwa paket parameter kurang ekspresif daripada metode parameter. (Lihat proposal yang ditautkan dari komentar pertama untuk detailnya.)

Apakah tipe hiper adalah ide yang bagus?

func getAddFunc (aType type) func(aType, aType) aType {
    return func(a, b aType) aType {
        return a+b
    }
}

Apakah tipe hiper adalah ide yang bagus?

Apa yang Anda gambarkan di sini cukup ketik parameterisasi ala C++ (yaitu, templat). Itu tidak mengetik-periksa secara modular karena tidak ada cara untuk mengetahui bahwa tipe aType memiliki operasi + dari informasi yang diberikan. Parameterisasi tipe terbatas seperti pada CLU, Haskell, Java, Genus adalah solusinya.

@ golang101 Saya memiliki proposal terperinci di sepanjang baris itu. Saya akan mengirim CL untuk menambahkannya ke daftar, tetapi sepertinya tidak akan diadopsi.

CL https://golang.org/cl/38731 menyebutkan masalah ini.

@andrewcmyers

Itu tidak mengetik-periksa secara modular karena tidak ada cara untuk mengetahui bahwa tipe aType memiliki operasi + dari informasi yang diberikan.

Tentu ada. Batasan itu tersirat dalam definisi fungsi, dan batasan bentuk itu dapat disebarkan ke semua pemanggil waktu kompilasi (transitif) dari getAddFunc .

Batasan bukan bagian dari _type_ Go — yaitu, tidak dapat dikodekan dalam sistem tipe bagian run-time bahasa — tetapi itu tidak berarti bahwa itu tidak dapat dievaluasi secara modular.

Menambahkan proposal saya sebagai 2016-09-compile-time-functions.md .

Saya tidak berharap itu akan diadopsi, tetapi setidaknya bisa menjadi referensi yang menarik.

@bcmils Saya merasa bahwa fungsi waktu kompilasi adalah ide yang kuat, terlepas dari pertimbangan obat generik. Misalnya, saya menulis pemecah sudoku yang membutuhkan popcount. Untuk mempercepatnya, saya menghitung popcount untuk berbagai kemungkinan nilai dan menyimpannya sebagai Go source . Ini adalah sesuatu yang mungkin dilakukan dengan go:generate . Tetapi jika ada fungsi waktu kompilasi, tabel pencarian itu juga dapat dihitung pada waktu kompilasi, menjaga kode yang dihasilkan mesin dari keharusan untuk dikomit ke repo. Secara umum, setiap jenis fungsi matematika memoizable cocok untuk tabel pencarian yang dibuat sebelumnya dengan fungsi waktu kompilasi.

Secara lebih spekulatif, seseorang mungkin juga ingin, misalnya, mengunduh definisi protobuf dari sumber kanonik dan menggunakannya untuk membangun tipe pada waktu kompilasi. Tapi mungkin itu terlalu banyak untuk dilakukan pada waktu kompilasi?

Saya merasa fungsi waktu kompilasi terlalu kuat dan terlalu lemah pada saat yang sama: mereka terlalu fleksibel dan dapat membuat kesalahan dengan cara yang aneh / memperlambat kompilasi seperti yang dilakukan template C++, tetapi di sisi lain mereka terlalu statis dan sulit untuk beradaptasi dengan hal-hal seperti fungsi kelas satu.

Untuk bagian kedua, saya tidak melihat cara Anda dapat membuat sesuatu seperti "sepotong fungsi yang memproses irisan dari tipe tertentu dan mengembalikan satu elemen", atau dalam sintaks ad-hoc []func<T>([]T) T , yang sangat mudah dilakukan pada dasarnya setiap bahasa fungsional yang diketik secara statis. Apa yang benar-benar dibutuhkan adalah nilai yang dapat mengambil tipe parametrik, bukan pembuatan kode tingkat kode sumber.

@bunsim

Untuk bagian kedua, saya tidak melihat cara Anda dapat membuat sesuatu seperti "sepotong fungsi yang memproses irisan dari tipe tertentu dan mengembalikan satu elemen",

Jika Anda berbicara tentang parameter tipe tunggal, dalam proposal saya yang akan ditulis:

const func SliceOfSelectors(T gotype) gotype { return []func([]T)T (type) }

Jika Anda berbicara tentang pencampuran parameter tipe dan parameter nilai, tidak, proposal saya tidak mengizinkan untuk itu: bagian dari titik fungsi waktu kompilasi adalah untuk dapat beroperasi pada nilai tanpa kotak, dan jenis parametrik run-time Saya pikir Anda menggambarkan cukup banyak membutuhkan tinju nilai.

Yup, tapi menurut saya hal seperti itu yang membutuhkan tinju harus diizinkan dengan tetap menjaga keamanan jenis, mungkin dengan sintaks khusus yang menunjukkan "kotak". Sebagian besar penambahan "generik" adalah untuk menghindari jenis-ketidakamanan interface{} bahkan ketika overhead interface{} tidak dapat dihindari. (Mungkin hanya mengizinkan konstruksi tipe parametrik tertentu dengan pointer dan tipe antarmuka yang "sudah" dikotak? Objek kotak Integer dll Java tidak sepenuhnya merupakan ide yang buruk, meskipun irisan tipe nilai rumit)

Saya hanya merasa seperti fungsi waktu kompilasi sangat mirip C++, dan akan sangat mengecewakan bagi orang-orang seperti saya yang mengharapkan Go2 memiliki sistem tipe parametrik modern yang didasarkan pada teori tipe suara daripada peretasan berdasarkan memanipulasi potongan kode sumber yang ditulis dalam bahasa tanpa generik.

@bcmils
Apa yang Anda usulkan tidak akan bersifat modular. Jika modul A menggunakan modul B, yang menggunakan modul C, yang menggunakan modul D, perubahan pada cara parameter tipe digunakan di D mungkin perlu disebarkan kembali ke A, bahkan jika pelaksana A tidak mengetahui bahwa D ada di dalam sistem. Kopling longgar yang disediakan oleh sistem modul akan melemah, dan perangkat lunak akan lebih rapuh. Ini adalah salah satu masalah dengan template C++.

Jika, di sisi lain, tanda tangan tipe menangkap persyaratan pada parameter tipe, seperti dalam bahasa seperti CLU, ML, Haskell, atau Genus, sebuah modul dapat dikompilasi tanpa akses ke internal modul yang bergantung padanya.

@bunsim

Bagian besar dari menambahkan "generik" sebenarnya adalah untuk menghindari tipe-ketidakamanan antarmuka{} bahkan ketika overhead antarmuka{} tidak dapat dihindari.

"tidak dapat dihindari" adalah relatif. Perhatikan bahwa overhead tinju adalah poin # 3 dalam posting Russ dari 2009 (https://research.swtch.com/generic).

mengharapkan Go2 memiliki sistem tipe parametrik modern yang didasarkan pada teori tipe suara daripada peretasan berdasarkan memanipulasi potongan kode sumber

"Teori tipe suara" yang baik adalah deskriptif, bukan preskriptif. Proposal saya khususnya diambil dari kalkulus lambda orde kedua (sepanjang baris Sistem F), di mana gotype adalah singkatan dari jenis type dan seluruh sistem jenis orde pertama diangkat ke dalam yang kedua -order ("waktu kompilasi") jenis.

Ini juga terkait dengan karya teori tipe modal Davies, Pfenning, et al di CMU. Untuk beberapa latar belakang, saya akan mulai dengan Analisis Modal Komputasi Bertahap dan Jenis Modal sebagai Spesifikasi Pementasan untuk Pembuatan Kode Run-time .

Memang benar bahwa teori tipe yang mendasari proposal saya kurang ditentukan secara formal daripada dalam literatur akademis, tetapi itu tidak berarti itu tidak ada.

@andrewcmyers

Jika modul A menggunakan modul B, yang menggunakan modul C, yang menggunakan modul D, perubahan pada cara parameter tipe digunakan di D mungkin perlu disebarkan kembali ke A, bahkan jika pelaksana A tidak mengetahui bahwa D ada di dalam sistem.

Itu sudah benar di Go hari ini: jika Anda perhatikan dengan cermat, Anda akan melihat bahwa file objek yang dihasilkan oleh kompiler untuk paket Go yang diberikan menyertakan informasi tentang bagian dependensi transitif yang memengaruhi API yang diekspor.

Kopling longgar yang disediakan oleh sistem modul akan melemah, dan perangkat lunak akan lebih rapuh.

Saya pernah mendengar argumen yang sama yang digunakan untuk menganjurkan mengekspor tipe interface daripada tipe konkret di Go API, dan kebalikannya ternyata lebih umum: abstraksi prematur membatasi tipe dan menghalangi ekstensi API. (Untuk satu contoh seperti itu, lihat #19584.) Jika Anda ingin mengandalkan garis argumen ini, saya pikir Anda perlu memberikan beberapa contoh konkret.

Ini adalah salah satu masalah dengan template C++.

Seperti yang saya lihat, masalah utama dengan template C++ adalah (tanpa urutan tertentu):

  • Ambiguitas sintaksis yang berlebihan.
    sebuah. Ambiguitas antara nama tipe dan nama nilai.
    B. Dukungan yang terlalu luas untuk kelebihan beban operator, yang menyebabkan melemahnya kemampuan untuk menyimpulkan kendala dari penggunaan operator.
  • Ketergantungan yang berlebihan pada resolusi yang berlebihan untuk metaprogramming (atau, secara setara, evolusi ad-hoc dari dukungan metaprogramming).
    sebuah. Terutama wrt referensi-runtuh aturan.
  • Penerapan prinsip SFINAE yang terlalu luas, menyebabkan kendala yang sangat sulit untuk disebarkan dan terlalu banyak persyaratan implisit dalam definisi tipe, yang menyebabkan pelaporan kesalahan yang sangat sulit.
  • Terlalu sering menggunakan token-paste dan inklusi tekstual (preprosesor C) alih-alih substitusi AST dan artefak kompilasi tingkat tinggi (yang untungnya tampaknya setidaknya sebagian ditangani dengan Modul).
  • Kurangnya bahasa bootstrap yang baik untuk kompiler C++, menyebabkan pelaporan kesalahan yang buruk dalam garis keturunan kompiler yang berumur panjang (misalnya, toolchain GCC).
  • Penggandaan (dan terkadang perkalian) nama yang dihasilkan dari pemetaan set operator ke "konsep" bernama berbeda (daripada memperlakukan operator itu sendiri sebagai kendala mendasar).

Saya telah mengkode dalam C++ dan selama satu dekade sekarang dan saya senang membahas kekurangan C++ secara panjang lebar, tetapi fakta bahwa dependensi program bersifat transitif tidak pernah berada jauh di dekat bagian atas daftar keluhan saya.

Di sisi lain, perlu memperbarui rantai dependensi O(N) hanya untuk menambahkan satu metode ke tipe di modul A dan dapat menggunakannya di modul D? Itu jenis masalah yang memperlambat saya secara teratur. Di mana parametrik dan kopling longgar bertentangan, saya akan memilih parametrik setiap hari.

Namun, saya sangat yakin bahwa metaprogramming dan polimorfisme parametrik harus dipisahkan, dan kebingungan C++ tentang mereka adalah akar penyebab mengapa template C++ mengganggu. Sederhananya, C++ mencoba untuk menerapkan ide teori tipe menggunakan makro pada dasarnya pada steroid, yang sangat bermasalah karena programmer suka menganggap template sebagai polimorfisme parametrik nyata dan terkena perilaku tak terduga. Fungsi waktu kompilasi adalah ide bagus untuk metaprogramming dan mengganti hack yang go generate , tapi saya tidak percaya itu harus menjadi cara yang diberkati untuk melakukan pemrograman generik.

Polimorfisme parametrik "nyata" membantu kopling longgar dan tidak boleh bertentangan dengannya. Itu juga harus terintegrasi erat dengan sistem tipe lainnya; misalnya mungkin harus diintegrasikan ke dalam sistem antarmuka saat ini, sehingga banyak penggunaan jenis antarmuka dapat ditulis ulang menjadi hal-hal seperti:

func <T io.Reader> ReadAll(in T)

yang harus menghindari overhead antarmuka (seperti penggunaan Rust), meskipun dalam hal ini tidak terlalu berguna.

Contoh yang lebih baik mungkin paket sort , di mana Anda dapat memiliki sesuatu seperti

func <T Comparable> Sort(slice []T)

di mana Comparable hanyalah antarmuka lama yang bagus yang dapat diimplementasikan oleh tipe. Sort kemudian dapat dipanggil pada sepotong tipe nilai yang mengimplementasikan Comparable , tanpa memasukkannya ke dalam tipe antarmuka.

@bcmills Ketergantungan transitif yang tidak dibatasi oleh sistem tipe, menurut saya, merupakan inti dari beberapa keluhan Anda tentang C++. Ketergantungan transitif tidak terlalu menjadi masalah jika Anda mengontrol modul A, B, C, dan D. Secara umum, Anda sedang mengembangkan modul A dan mungkin hanya sedikit menyadari bahwa modul D ada di bawah sana, dan sebaliknya, pengembang D mungkin tidak menyadari A. Jika modul D sekarang, tanpa membuat perubahan apa pun pada deklarasi yang terlihat di D, mulai menggunakan beberapa operator baru pada parameter tipe—atau hanya menggunakan parameter tipe itu sebagai argumen tipe ke modul baru E dengan miliknya sendiri batasan implisit—kendala tersebut akan meresap ke semua klien, yang mungkin tidak menggunakan argumen tipe yang memenuhi batasan. Tidak ada yang memberi tahu pengembang D bahwa mereka mengacaukannya. Akibatnya, Anda memiliki semacam inferensi tipe global, dengan semua kesulitan debugging yang menyertainya.

Saya percaya pendekatan yang kami ambil di Genus [ PLDI'15 ] jauh lebih baik. Parameter tipe memiliki batasan eksplisit, tetapi ringan (saya mengerti maksud Anda tentang mendukung batasan operasi; CLU menunjukkan bagaimana melakukannya dengan benar sejak tahun 1977). Pengecekan tipe genus sepenuhnya modular. Kode generik dapat dikompilasi hanya sekali untuk mengoptimalkan ruang kode atau khusus untuk argumen tipe tertentu untuk kinerja yang baik.

@andrewcmyers

Jika modul D sekarang, tanpa membuat perubahan apa pun pada deklarasi yang terlihat di D, mulai menggunakan beberapa operator baru pada parameter tipe […] [klien] mungkin tidak menggunakan argumen tipe yang memenuhi batasan. Tidak ada yang memberi tahu pengembang D bahwa mereka mengacaukannya.

Tentu, tapi itu sudah berlaku untuk banyak kendala implisit di Go yang terlepas dari mekanisme pemrograman umum apa pun.

Misalnya, suatu fungsi dapat menerima parameter tipe antarmuka dan awalnya memanggil metodenya secara berurutan. Jika fungsi itu kemudian berubah untuk memanggil metode tersebut secara bersamaan (dengan memunculkan goroutine tambahan), batasan "harus aman untuk penggunaan bersamaan" tidak tercermin dalam sistem tipe.

Demikian pula, sistem tipe Go saat ini tidak menentukan batasan pada masa pakai variabel: beberapa implementasi io.Writer secara keliru menganggap mereka dapat menyimpan referensi ke irisan yang diteruskan dan membacanya nanti (misalnya dengan melakukan penulisan aktual secara asinkron di goroutine latar belakang), tetapi hal itu menyebabkan balapan data jika pemanggil Write mencoba menggunakan kembali backing slice yang sama untuk Write berikutnya.

Atau fungsi yang menggunakan sakelar tipe mungkin mengambil jalur berbeda dari metode yang ditambahkan ke salah satu tipe di sakelar.

Atau fungsi yang memeriksa kode kesalahan tertentu mungkin rusak jika fungsi yang menghasilkan kesalahan mengubah cara melaporkan kondisi tersebut. (Misalnya, lihat https://github.com/golang/go/issues/19647.)

Atau fungsi yang memeriksa jenis kesalahan tertentu mungkin rusak jika pembungkus di sekitar kesalahan ditambahkan atau dihapus (seperti yang terjadi pada paket net standar di Go 1.5).

Atau buffering pada saluran yang diekspos dalam API dapat berubah, menyebabkan kebuntuan dan/atau balapan.

...dan seterusnya.

Go tidak biasa dalam hal ini: batasan implisit ada di mana-mana dalam program dunia nyata.


Jika Anda mencoba menangkap semua batasan yang relevan dalam anotasi eksplisit, maka Anda akan berakhir di salah satu dari dua arah.

Di satu arah, Anda membangun sistem tipe dan anotasi dependen yang kompleks dan sangat komprehensif, dan anotasi tersebut akhirnya merekapitulasi sebagian besar kode yang mereka anotasi. Seperti yang saya harap Anda dapat melihat dengan jelas, arah itu sama sekali tidak sesuai dengan desain bahasa Go lainnya: Go lebih menyukai kesederhanaan spesifikasi dan keringkasan kode daripada pengetikan statis yang komprehensif.

Di arah lain, anotasi eksplisit hanya akan mencakup sebagian dari batasan yang relevan untuk API tertentu. Sekarang anotasi memberikan rasa aman yang salah: kode masih dapat rusak karena perubahan batasan implisit, tetapi adanya batasan eksplisit menyesatkan pengembang untuk berpikir bahwa setiap perubahan "jenis-aman" juga mempertahankan kompatibilitas.


Tidak jelas bagi saya mengapa stabilitas API semacam itu perlu dicapai melalui anotasi kode sumber eksplisit: jenis stabilitas API yang Anda gambarkan juga dapat dicapai (dengan lebih sedikit redundansi dalam kode) melalui analisis kode sumber. Misalnya, Anda dapat membayangkan alat api menganalisis kode dan menghasilkan serangkaian batasan yang jauh lebih kaya daripada yang dapat diekspresikan dalam sistem tipe bahasa formal, dan memberikan alat guru kemampuan untuk mengkueri kumpulan batasan yang dihitung untuk setiap fungsi, metode, atau parameter API yang diberikan.

@bcmils Bukankah Anda menjadikan yang sempurna sebagai musuh dari yang baik? Ya, ada batasan implisit yang sulit ditangkap dalam sistem tipe. (Dan desain modular yang baik menghindari pengenalan batasan implisit seperti itu jika memungkinkan.) Akan sangat bagus jika memiliki analisis menyeluruh yang dapat memeriksa semua properti yang ingin Anda periksa secara statis -- dan memberikan penjelasan yang jelas dan tidak menyesatkan kepada pemrogram tentang di mana mereka sedang membuat kesalahan. Bahkan dengan kemajuan terbaru pada diagnosis kesalahan otomatis dan lokalisasi , saya tidak menahan napas. Untuk satu hal, alat analisis hanya dapat menganalisis kode yang Anda berikan. Pengembang tidak selalu memiliki akses ke semua kode yang mungkin terhubung dengan kode mereka.

Jadi di mana ada kendala yang mudah ditangkap dalam sistem tipe, mengapa tidak memberi pemrogram kemampuan untuk menuliskannya? Kami memiliki 40 tahun pengalaman pemrograman dengan parameter tipe yang dibatasi secara statis. Ini adalah anotasi statis intuitif sederhana yang terbayar.

Setelah Anda mulai membangun perangkat lunak yang lebih besar yang melapisi modul perangkat lunak, Anda mulai ingin menulis komentar yang menjelaskan batasan implisit seperti itu. Dengan asumsi ada cara yang baik dan dapat diperiksa untuk mengekspresikannya, mengapa tidak membiarkan kompiler terlibat dalam lelucon sehingga dapat membantu Anda?

Saya perhatikan bahwa beberapa contoh kendala implisit Anda lainnya melibatkan penanganan kesalahan. Saya pikir pemeriksaan pengecualian statis ringan kami [ PLDI 2016 ] akan membahas contoh-contoh ini.

@andrewcmyers

Jadi di mana ada kendala yang mudah ditangkap dalam sistem tipe, mengapa tidak memberi pemrogram kemampuan untuk menuliskannya?
[…]
Setelah Anda mulai membangun perangkat lunak yang lebih besar yang melapisi modul perangkat lunak, Anda mulai ingin menulis komentar yang menjelaskan batasan implisit seperti itu. Dengan asumsi ada cara yang baik dan dapat diperiksa untuk mengekspresikannya, mengapa tidak membiarkan kompiler terlibat dalam lelucon sehingga dapat membantu Anda?

Saya sebenarnya setuju sepenuhnya dengan poin ini, dan saya sering menggunakan argumen serupa dalam hal manajemen memori. (Jika Anda tetap harus mendokumentasikan invarian pada aliasing dan retensi data, mengapa tidak menerapkan invarian tersebut pada waktu kompilasi?)

Tapi saya akan mengambil argumen itu satu langkah lebih jauh: kebalikannya juga berlaku! Jika Anda _tidak_ perlu menulis komentar untuk kendala (karena jelas dalam konteks untuk manusia yang bekerja dengan kode), mengapa Anda harus menulis komentar itu untuk kompiler? Terlepas dari preferensi pribadi saya, penggunaan pengumpulan sampah dan nilai nol oleh Go dengan jelas menunjukkan bias terhadap "tidak mengharuskan pemrogram untuk menyatakan invarian yang jelas". Mungkin saja pemodelan gaya Genus dapat mengekspresikan banyak kendala yang akan diungkapkan dalam komentar, tetapi bagaimana hasilnya dalam hal menghilangkan kendala yang juga akan dihilangkan dalam komentar?

Tampaknya bagi saya bahwa model gaya Genus lebih dari sekadar komentar: mereka benar-benar mengubah semantik kode dalam beberapa kasus, mereka tidak hanya membatasinya. Sekarang kita akan memiliki dua mekanisme yang berbeda — antarmuka dan model tipe — untuk parameterisasi perilaku. Itu akan mewakili perubahan besar dalam bahasa Go: kami telah menemukan beberapa praktik terbaik untuk antarmuka dari waktu ke waktu (seperti "menentukan antarmuka di sisi konsumen") dan tidak jelas bahwa pengalaman itu akan diterjemahkan ke sistem yang sangat berbeda, bahkan mengabaikan kompatibilitas Go 1.

Lebih jauh lagi, salah satu keunggulan Go adalah spesifikasinya dapat dibaca (dan dipahami secara luas) dalam satu sore. Tidak jelas bagi saya bahwa sistem batasan gaya Genus dapat ditambahkan ke bahasa Go tanpa memperumitnya secara substansial — saya akan penasaran untuk melihat proposal konkret untuk perubahan pada spesifikasi.

Inilah titik data yang menarik untuk "pemrograman meta". Akan lebih baik untuk tipe tertentu dalam paket sync dan atomic — yaitu, atomic.Value dan sync.Map — untuk mendukung metode CompareAndSwap , tetapi itu hanya berfungsi untuk tipe yang kebetulan sebanding. Sisa dari atomic.Value dan sync.Map API tetap berguna tanpa metode tersebut, jadi untuk kasus penggunaan itu kita memerlukan sesuatu seperti SFINAE (atau jenis API yang ditentukan bersyarat lainnya) atau harus jatuh kembali ke hierarki tipe yang lebih kompleks.

Saya ingin membuang ide sintaksis kreatif ini menggunakan suku kata asli.

@bcmils Bisakah Anda menjelaskan lebih lanjut tentang tiga poin ini?

  1. Ambiguitas antara nama tipe dan nama nilai.
  2. Dukungan yang terlalu luas untuk kelebihan beban operator
    3. Ketergantungan yang berlebihan pada resolusi yang berlebihan untuk metaprogramming

@mahdix Tentu.

  1. Ambiguitas antara nama tipe dan nama nilai.

Artikel ini memberikan pengantar yang bagus. Untuk mengurai program C++, Anda harus mengetahui nama mana yang merupakan tipe dan mana yang merupakan nilai. Saat Anda mengurai program C++ berpola, Anda tidak memiliki informasi yang tersedia untuk anggota parameter templat.

Masalah serupa muncul di Go untuk literal komposit, tetapi ambiguitas adalah antara nilai dan nama bidang daripada nilai dan tipe. Dalam kode Go ini:

const a = someValue
x := T{a: b}

apakah a adalah nama bidang literal, atau apakah itu konstanta a yang digunakan sebagai kunci peta atau indeks larik?

  1. Dukungan yang terlalu luas untuk kelebihan beban operator

Pencarian yang bergantung pada argumen adalah tempat yang baik untuk memulai. Kelebihan operator di C++ dapat terjadi sebagai metode pada tipe penerima atau sebagai fungsi bebas di salah satu dari beberapa ruang nama, dan aturan untuk menyelesaikan kelebihan tersebut cukup rumit.

Ada banyak cara untuk menghindari kerumitan itu, tetapi yang paling sederhana (seperti yang dilakukan Go saat ini) adalah dengan melarang operator kelebihan beban sepenuhnya.

  1. Ketergantungan yang berlebihan pada resolusi yang berlebihan untuk metaprogramming

Pustaka <type_traits> adalah tempat yang baik untuk memulai. Lihat implementasi di lingkungan ramah Anda libc++ untuk melihat bagaimana resolusi yang berlebihan berperan.

Jika Go mendukung metaprogramming (dan bahkan itu sangat diragukan), saya tidak akan mengharapkannya melibatkan resolusi yang berlebihan sebagai operasi dasar untuk menjaga definisi bersyarat.

@bcmils
Karena saya belum pernah menggunakan C++, dapatkah Anda menjelaskan di mana kelebihan beban operator melalui penerapan 'antarmuka' yang telah ditentukan sebelumnya dalam hal kompleksitas. Python dan Kotlin adalah contohnya.

Saya pikir ADL itu sendiri adalah masalah besar dengan templat C++ yang sebagian besar tidak disebutkan, karena mereka memaksa kompiler untuk menunda resolusi semua nama hingga waktu instantiasi, dan dapat menghasilkan bug yang sangat halus, sebagian karena "ideal" dan " kompiler lazy" berperilaku berbeda di sini dan standar mengizinkannya. Fakta bahwa ia mendukung kelebihan beban operator sejauh ini bukanlah bagian terburuknya.

Proposal ini didasarkan pada Template, sistem untuk ekspansi makro tidak akan cukup? Saya tidak berbicara tentang go generate atau proyek seperti gottemplate. Saya berbicara tentang lebih banyak seperti ini:

macro MacroFoo(stmt ast.Statement) {
    ....
}

Makro bisa mengurangi boilerplate dan penggunaan refleksi.

Saya pikir C++ adalah contoh yang cukup baik bahwa obat generik tidak boleh didasarkan pada templat atau makro. Terutama mengingat Go memiliki hal-hal seperti fungsi anonim yang benar-benar tidak dapat "dibuat" pada waktu kompilasi kecuali sebagai pengoptimalan.

@samadadi Anda bisa menyampaikan maksud Anda tanpa mengatakan "ada apa dengan kalian". Karena itu, argumen kompleksitas telah dikemukakan beberapa kali.

Go bukanlah bahasa pertama yang mencoba mencapai kesederhanaan dengan menghilangkan dukungan untuk polimorfisme parametrik (generik), meskipun fitur tersebut menjadi semakin penting selama 40 tahun terakhir -- menurut pengalaman saya, ini adalah pokok dari kursus pemrograman semester kedua.

Masalah dengan tidak memiliki fitur dalam bahasa adalah bahwa programmer akhirnya beralih ke solusi yang lebih buruk. Misalnya, pemrogram Go sering menulis templat kode yang diperluas secara makro untuk menghasilkan kode "asli" untuk berbagai jenis yang diinginkan. Tetapi bahasa pemrograman yang sebenarnya adalah yang Anda ketik, bukan yang dilihat oleh kompiler. Jadi strategi ini secara efektif berarti Anda menggunakan bahasa (tidak lagi standar) yang memiliki semua kerapuhan dan kode yang membengkak dari template C++.

Seperti yang tercantum di https://blog.golang.org/toward-go2 kita perlu menyediakan "laporan pengalaman", sehingga kebutuhan dan tujuan desain dapat ditentukan. Bisakah Anda meluangkan beberapa menit dan mendokumentasikan kasus makro yang telah Anda amati?

Harap menjaga bug ini pada topik dan sipil. Dan lagi, https://golang.org/wiki/NoMeToo. Harap hanya berkomentar jika Anda memiliki informasi unik dan konstruktif untuk ditambahkan.

@mandolyte Sangat mudah untuk menemukan penjelasan terperinci di web yang menganjurkan pembuatan kode sebagai pengganti (sebagian) untuk obat generik:
https://appliedgo.net/generics/
https://www.calhoun.io/using-code-generation-to-survive-without-generics-in-go/
http://blog.ralch.com/tutorial/golang-code-generation-and-generics/

Jelas ada banyak orang di luar sana yang mengambil pendekatan ini.

@andrewcmyers , ada beberapa batasan serta peringatan kenyamanan saat menggunakan pembuatan kode TAPI .
Secara umum - jika Anda yakin pendekatan ini terbaik/cukup baik, saya pikir upaya untuk memungkinkan generasi yang agak mirip dari dalam rantai alat go akan menjadi berkah.

  • Pengoptimalan kompiler mungkin menjadi tantangan dalam kasus ini, tetapi runtime akan konsisten, DAN pemeliharaan kode, pengalaman pengguna (kesederhanaan...), praktik terbaik standar, dan standar kode terpadu dapat dipertahankan.
    Selain itu - semua rantai alat akan tetap sama, selain dari alat debugging (profiler , langkah debugger dll) yang akan melihat baris kode yang tidak ditulis oleh pengembang, tapi itu sedikit seperti masuk ke kode ASM saat debugging - hanya ini adalah kode yang dapat dibaca :) .

Kelemahan - tidak ada preseden (yang saya tahu) untuk pendekatan ini di dalam go tool chain .

Singkatnya - pertimbangkan pembuatan kode sebagai bagian dari proses build , seharusnya tidak terlalu rumit, cukup aman, runtime dioptimalkan, dapat menjaga kesederhanaan dan perubahan bahasa yang sangat kecil.

IMHO: Ini adalah kompromi yang mudah dicapai, dengan harga murah.

Untuk lebih jelasnya, saya tidak menganggap pembuatan kode gaya makro, baik yang dilakukan dengan gen, cpp, gofmt -r, atau alat makro/templat lainnya, sebagai solusi yang baik untuk masalah generik bahkan jika distandarisasi. Ini memiliki masalah yang sama dengan template C++: kode mengasapi, kurangnya pemeriksaan tipe modular, dan kesulitan debugging. Ini menjadi lebih buruk saat Anda mulai, seperti biasa, membangun kode generik dalam hal kode generik lainnya. Menurut saya, keuntungannya terbatas: itu akan membuat hidup relatif sederhana untuk penulis kompiler Go dan menghasilkan kode yang efisien — kecuali ada tekanan cache instruksi, situasi yang sering terjadi dalam perangkat lunak modern!

Saya pikir intinya adalah bahwa pembuatan kode digunakan untuk menggantikan
obat generik, jadi obat generik harus berusaha memecahkan sebagian besar kasus penggunaan tersebut.

Pada Rabu, 26 Juli 2017, 22:41 Andrew Myers, [email protected] menulis:

Untuk lebih jelasnya, saya tidak mempertimbangkan pembuatan kode gaya makro, apakah sudah selesai
dengan gen, cpp, gofmt -r, atau alat makro/templat lainnya, untuk menjadi baik
solusi untuk masalah generik bahkan jika standar. Ini memiliki hal yang sama
masalah sebagai template C++: kode mengasapi, kurangnya pemeriksaan tipe modular, dan
kesulitan debugging. Ini menjadi lebih buruk saat Anda mulai, seperti biasa, membangun
kode generik dalam hal kode generik lainnya. Menurut saya, kelebihannya adalah
terbatas: itu akan membuat hidup relatif sederhana untuk penulis kompiler Go
dan itu menghasilkan kode yang efisien — kecuali ada cache instruksi
tekanan, situasi yang sering terjadi dalam perangkat lunak modern!


Anda menerima ini karena Anda berkomentar.
Balas email ini secara langsung, lihat di GitHub
https://github.com/golang/go/issues/15292#issuecomment-318242016 , atau bisu
benang
https://github.com/notifications/unsubscribe-auth/AT4HVb2SPMpe5dlEDUQeadIRKPaB74zoks5sR_jSgaJpZM4IG-xv
.

Tidak diragukan lagi pembuatan kode bukanlah solusi NYATA bahkan jika dibungkus dengan beberapa dukungan bahasa untuk membuat tampilan dan nuansa sebagai "bagian dari bahasa"

Maksud saya adalah SANGAT hemat biaya.

Btw, jika Anda melihat beberapa pengganti pembuatan kode, Anda dapat dengan mudah melihat bagaimana mereka bisa jauh lebih mudah dibaca, lebih cepat, dan tidak memiliki beberapa konsep yang salah (misalnya iterasi pada array pointer vs nilai) jika bahasa memberi mereka alat yang lebih baik untuk ini.

Dan mungkin itu jalan yang lebih baik untuk diselesaikan dalam jangka pendek, yang tidak akan terasa seperti tambalan:
sebelum memikirkan "dukungan generik terbaik yang juga akan menjadi idiomatik untuk digunakan" (saya percaya beberapa implementasi di atas akan memakan waktu bertahun-tahun untuk mencapai integrasi penuh), implementasikan beberapa set fungsi yang didukung "dalam bahasa" yang bagaimanapun juga diperlukan (seperti build in struktur salinan dalam) akan membuat solusi penghasil kode ini jauh lebih bermanfaat.

Setelah membaca proposal generik oleh @bcmills dan @ianlancetaylor , saya membuat pengamatan berikut:

Fungsi Waktu Kompilasi dan Tipe Kelas Satu

Saya menyukai ide evaluasi waktu kompilasi, tetapi saya tidak melihat manfaat dari membatasinya pada fungsi murni. Proposal ini memperkenalkan gotype , tetapi membatasi penggunaannya pada fungsi const dan tipe data apa pun yang ditentukan dalam cakupan fungsi. Dari perspektif pengguna perpustakaan, instantiasi terbatas pada fungsi konstruktor seperti "Baru", dan mengarah ke tanda tangan fungsi seperti ini:

const func New(K, V gotype, hashfn Hashfn(K), eqfn Eqfn(K)) func()*Hashmap(K, V, hashfn, eqfn)

Tipe kembalian di sini tidak dapat dipisahkan menjadi tipe fungsi karena kita terbatas pada fungsi murni. Selain itu, tanda tangan mendefinisikan dua "tipe" baru dalam tanda tangan itu sendiri (K dan V), yang berarti bahwa untuk mengurai satu parameter, kita harus menguraikan seluruh daftar parameter. Ini bagus untuk kompiler, tetapi saya ingin tahu apakah menambah kompleksitas pada API publik suatu paket.

Ketik Parameter di Go

Jenis parameter memungkinkan untuk sebagian besar kasus penggunaan pemrograman generik, misalnya kemampuan untuk mendefinisikan struktur data generik dan operasi atas tipe data yang berbeda. Proposal secara lengkap mencantumkan peningkatan pada pemeriksa tipe yang akan diperlukan untuk menghasilkan kesalahan kompilasi yang lebih baik, waktu kompilasi yang lebih cepat, dan binari yang lebih kecil.

Di bawah bagian "Pemeriksa Jenis," proposal juga mencantumkan beberapa batasan jenis yang berguna untuk mempercepat proses, seperti "Dapat diindeks", "Sebanding", "Dapat Dipanggil", "Komposit", dll... Yang tidak saya mengerti adalah mengapa tidak mengizinkan pengguna menentukan batasan tipe mereka sendiri? Usulan tersebut menyatakan bahwa

Tidak ada batasan tentang bagaimana tipe berparameter dapat digunakan dalam fungsi berparameter.

Namun, jika pengidentifikasi memiliki lebih banyak batasan yang terkait dengannya, bukankah itu akan membantu kompiler? Mempertimbangkan:

HashMap[Anything,Anything] // Compiler must always compare the implementation and usages to make sure this is valid.

vs

HashMap[Comparable,Anything] // Compiler can first filter out instantiations for incomparable types before running an exhaustive check.

Memisahkan batasan tipe dari parameter tipe dan mengizinkan batasan yang ditentukan pengguna juga dapat meningkatkan keterbacaan, membuat paket generik lebih mudah dipahami. Menariknya, kekurangan yang tercantum di akhir proposal mengenai kompleksitas aturan pengurangan tipe sebenarnya dapat dikurangi jika aturan tersebut secara eksplisit didefinisikan oleh pengguna.

@smasher164

Saya menyukai ide evaluasi waktu kompilasi, tetapi saya tidak melihat manfaat dari membatasinya pada fungsi murni.

Manfaatnya adalah memungkinkan kompilasi terpisah. Jika fungsi waktu kompilasi dapat mengubah status global, maka kompiler harus memiliki status tersebut tersedia, atau membuat jurnal pengeditannya sedemikian rupa sehingga penaut dapat mengurutkannya pada waktu tautan. Jika fungsi waktu kompilasi dapat mengubah status lokal, maka kita memerlukan beberapa cara untuk melacak status mana yang lokal vs. global. Keduanya menambah kerumitan, dan tidak jelas apakah keduanya akan memberikan manfaat yang cukup untuk mengimbanginya.

@smasher164

Apa yang saya tidak mengerti adalah mengapa tidak mengizinkan pengguna menentukan batasan tipe mereka sendiri?

Pembatasan jenis dalam proposal itu sesuai dengan operasi dalam sintaks bahasa. Itu mengurangi luas permukaan fitur baru: tidak perlu menentukan sintaks tambahan untuk tipe pembatas, karena semua batasan sintaksis dapat disimpulkan dari penggunaan.

jika pengidentifikasi memiliki lebih banyak batasan yang terkait dengannya, bukankah itu akan membantu kompiler?

Bahasa harus dirancang untuk penggunanya, bukan untuk penyusun-penulis.

tidak perlu menentukan sintaks tambahan untuk tipe pembatas karena semua batasan sintaksis dapat disimpulkan dari penggunaan.

Ini adalah rute C++ turun. Ini membutuhkan analisis program global untuk mengidentifikasi penggunaan yang relevan. Kode tidak dapat dijelaskan oleh programmer secara modular, dan pesan kesalahan bertele-tele dan tidak dapat dipahami.

Ini bisa sangat mudah dan ringan untuk menentukan operasi yang dibutuhkan. Lihat CLU (1977) sebagai contoh.

@andrewcmyers

Ini membutuhkan analisis program global untuk mengidentifikasi penggunaan yang relevan. Kode tidak dapat dijelaskan oleh programmer secara modular,

Itu menggunakan definisi khusus "modular", yang menurut saya tidak universal seperti yang Anda asumsikan. Di bawah proposal 2013, setiap fungsi atau jenis akan memiliki serangkaian batasan yang jelas yang disimpulkan dari bawah ke atas dari paket yang diimpor, dengan cara yang persis sama dengan run-time (dan batasan run-time) dari fungsi non-parametrik diturunkan dari bawah- naik dari rantai panggilan hari ini.

Anda mungkin dapat menanyakan batasan yang disimpulkan menggunakan guru atau alat serupa, dan itu dapat menjawab pertanyaan tersebut menggunakan informasi lokal dari metadata paket yang diekspor.

dan pesan kesalahan bertele-tele dan tidak dapat dipahami.

Kami memiliki beberapa contoh (GCC dan MSVC) yang menunjukkan bahwa pesan kesalahan yang dibuat secara naif tidak dapat dipahami. Saya pikir itu berlebihan untuk menganggap bahwa pesan kesalahan untuk kendala implisit secara intrinsik buruk.

Saya pikir kelemahan terbesar dari batasan yang disimpulkan adalah mereka membuatnya mudah untuk menggunakan tipe dengan cara yang memperkenalkan batasan tanpa sepenuhnya memahaminya. Dalam kasus terbaik, ini hanya berarti bahwa pengguna Anda mungkin mengalami kegagalan waktu kompilasi yang tidak terduga, tetapi dalam kasus terburuk, ini berarti Anda dapat merusak paket untuk konsumen dengan memperkenalkan batasan baru secara tidak sengaja. Batasan yang ditentukan secara eksplisit akan menghindari hal ini.

Saya pribadi juga tidak merasa bahwa batasan eksplisit tidak sejalan dengan pendekatan Go yang ada, karena antarmuka adalah batasan tipe runtime eksplisit, meskipun mereka memiliki ekspresivitas terbatas.

Kami memiliki beberapa contoh (GCC dan MSVC) yang menunjukkan bahwa pesan kesalahan yang dibuat secara naif tidak dapat dipahami. Saya pikir itu berlebihan untuk menganggap bahwa pesan kesalahan untuk kendala implisit secara intrinsik buruk.

Daftar kompiler di mana inferensi tipe non-lokal - yang Anda usulkan - menghasilkan pesan kesalahan yang buruk sedikit lebih lama dari itu. Ini termasuk SML, OCaml, dan GHC, di mana banyak upaya telah dilakukan untuk meningkatkan pesan kesalahan mereka dan di mana setidaknya ada beberapa struktur modul eksplisit yang membantu. Anda mungkin dapat melakukan lebih baik, dan jika Anda menemukan algoritme untuk pesan kesalahan yang baik dengan skema yang Anda usulkan, Anda akan memiliki publikasi yang bagus. Sebagai titik awal menuju algoritme itu, Anda mungkin menemukan makalah POPL 2014 dan PLDI 2015 kami tentang lokalisasi kesalahan berguna. Mereka kurang lebih canggih.

karena semua kendala sintaksis dapat disimpulkan dari penggunaan.

Bukankah itu membatasi luasnya program generik yang dapat diperiksa tipenya? Misalnya, perhatikan bahwa proposal tipe-params tidak menentukan batasan "Dapat diubah". Dalam bahasa saat ini, ini akan sesuai dengan irisan atau saluran, tetapi tipe komposit (misalnya daftar tertaut) tidak selalu memenuhi persyaratan tersebut. Mendefinisikan antarmuka seperti

type Iterable[T] interface {
    Next() T
}

membantu kasus daftar tertaut, tetapi sekarang irisan bawaan dan jenis saluran harus diperluas untuk memenuhi antarmuka ini.

Batasan yang mengatakan "Saya menerima kumpulan semua jenis yang dapat berupa Iterable, irisan, atau saluran" tampaknya seperti situasi win-win-win bagi pengguna, pembuat paket, dan pelaksana kompiler. Poin yang saya coba sampaikan adalah bahwa batasan adalah superset dari program yang valid secara sintaksis, dan beberapa mungkin tidak masuk akal dari perspektif bahasa, tetapi hanya dari perspektif API.

Bahasa harus dirancang untuk penggunanya, bukan untuk penyusun-penulis.

Saya setuju, tapi mungkin saya harus mengungkapkannya secara berbeda. Peningkatan efisiensi kompiler bisa menjadi efek samping dari batasan yang ditentukan pengguna. Manfaat utamanya adalah keterbacaan, karena pengguna memiliki gagasan yang lebih baik tentang perilaku API mereka daripada kompiler. Tradeoff di sini adalah bahwa program generik harus sedikit lebih eksplisit tentang apa yang mereka terima.

Bagaimana jika alih-alih

type Iterable[T] interface {
    Next() T
}

kami memisahkan gagasan "antarmuka" dari "kendala". Maka kita mungkin memiliki

type T generic

type Iterable class {
    Next() T
}

di mana "class" berarti kelas tipe gaya Haskell, bukan kelas gaya Java.

Memiliki "kelas tipe" yang terpisah dari "antarmuka" dapat membantu menjernihkan beberapa ketidak-ortogonalan dari kedua gagasan tersebut. Kemudian Sortable (mengabaikan sort.Interface) mungkin terlihat seperti:

type T generic

type Comparable class {
    Less(a, b T) bool
}

type Sortable class {
    Next() Comparable
}

Berikut adalah beberapa umpan balik ke bagian "Ketik kelas dan konsep" di Genus oleh @andrewcmyers dan penerapannya untuk Go.

Bagian ini membahas keterbatasan kelas tipe dan konsep, dengan menyatakan:

pertama, kepuasan kendala harus disaksikan secara unik

Saya tidak yakin saya memahami batasan ini. Tidakkah mengikat batasan ke pengidentifikasi terpisah mencegahnya menjadi unik untuk tipe tertentu? Tampak bagi saya bahwa klausa "di mana" dalam Genus pada dasarnya membangun tipe/batasan dari batasan yang diberikan, tetapi ini tampaknya analog dengan membuat instance variabel dari tipe yang diberikan. Batasan dengan cara ini menyerupai sejenis .

Berikut adalah penyederhanaan definisi kendala yang dramatis, yang disesuaikan dengan Go:

kind Any interface{} // accepts any type that satisfies interface{}.
type T Any // Declare a type of Any kind. Also binds it to an identifier.
kind Eq T == T // accepts any type for which equality is defined.

Jadi deklarasi peta akan muncul sebagai:

type Map[K Eq, V Any] struct {
}

di mana di Genus, itu bisa terlihat seperti:

type Map[K, V] where Eq[K], Any[V] struct {
}

dan dalam proposal Type-Params yang ada akan terlihat seperti:

type Map[K,V] struct {
}

Saya pikir kita semua bisa setuju bahwa mengizinkan batasan untuk memanfaatkan sistem tipe yang ada dapat menghilangkan tumpang tindih antara fitur bahasa, dan membuatnya mudah untuk memahami yang baru.

dan kedua, model mereka menentukan bagaimana mengadaptasi satu tipe, sedangkan dalam bahasa dengan subtipe, setiap tipe yang diadaptasi secara umum mewakili semua subtipenya.

Batasan ini tampaknya kurang relevan dengan Go karena bahasa tersebut sudah memiliki aturan konversi yang baik antara tipe bernama/tidak bernama dan antarmuka yang tumpang tindih.

Contoh yang diberikan mengusulkan model sebagai solusi, yang tampaknya menjadi fitur yang berguna tetapi tidak diperlukan untuk Go. Jika perpustakaan mengharapkan suatu tipe untuk mengimplementasikan http.Handler misalnya, dan pengguna menginginkan perilaku yang berbeda tergantung pada konteksnya, menulis adaptor itu sederhana:

type handleFunc func(http.ResponseWriter, *http.Request)
func (f handlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) { f(w,r) }

Sebenarnya, inilah yang dilakukan perpustakaan standar .

@smasher164

pertama, kepuasan kendala harus disaksikan secara unik
Saya tidak yakin saya memahami batasan ini. Tidakkah mengikat batasan ke pengidentifikasi terpisah mencegahnya menjadi unik untuk tipe tertentu?

Idenya adalah bahwa di Genus Anda dapat memenuhi batasan yang sama dengan tipe yang sama di lebih dari satu cara, tidak seperti di Haskell. Misalnya, jika Anda memiliki HashSet[T] , Anda dapat menulis HashSet[String] ke string hash dengan cara biasa tetapi HashSet[String with CaseInsens] ke hash dan membandingkan string dengan CaseInsens model, yang mungkin memperlakukan string dengan cara yang tidak peka huruf besar-kecil. Genus sebenarnya membedakan kedua jenis ini; ini mungkin berlebihan untuk Go. Bahkan jika sistem tipe tidak melacaknya, tampaknya masih penting untuk dapat mengesampingkan operasi default yang disediakan oleh suatu tipe.

kind Any interface{} // menerima semua tipe yang memenuhi antarmuka{}.
type T Any // Mendeklarasikan tipe Any kind. Juga mengikatnya ke pengidentifikasi.
jenis Persamaan T == T // menerima jenis apa pun yang persamaannya didefinisikan.
ketik Map[K Eq, V Any] struct { ...
}

Persamaan moral dari ini dalam Genus adalah:

constraint Any[T] {}
// Just use Any as if it were a type
constraint Eq[K] {
   boolean equals(K);
}
class Map[K, V] where Eq[K] { ... }

Di Familia kami hanya akan menulis:

interface Eq {
    boolean equals(This);
}
class Map[K where Eq, V] { ... }

Sunting: mencabut ini demi solusi berbasis refleksi seperti yang dijelaskan dalam # 4146 Solusi berbasis generik seperti yang saya jelaskan di bawah tumbuh secara linier dalam jumlah komposisi. Sementara solusi berbasis refleksi akan selalu memiliki cacat kinerja, ia dapat mengoptimalkan dirinya sendiri saat runtime sehingga cacatnya konstan terlepas dari jumlah komposisi.

Ini bukan proposal tetapi kasus penggunaan potensial untuk dipertimbangkan saat merancang proposal.

Dua hal yang umum dalam kode Go hari ini

  • membungkus nilai antarmuka untuk menyediakan fungsionalitas tambahan (membungkus http.ResponseWriter untuk kerangka kerja)
  • memiliki metode opsional yang terkadang dimiliki nilai antarmuka (seperti Temporary() bool pada net.Error )

Ini baik dan berguna tetapi mereka tidak bercampur. Setelah Anda membungkus antarmuka, Anda kehilangan kemampuan untuk mengakses metode apa pun yang tidak ditentukan pada jenis pembungkusnya. Artinya, diberikan

type MyError struct {
  error
  extraContext extraContextType
}
func (m MyError) Error() string {
  return fmt.Sprintf("%s: %s", m.extraContext, m.error)
}

Jika Anda membungkus kesalahan dalam struct itu, Anda menyembunyikan metode tambahan apa pun pada kesalahan asli.

Jika Anda tidak membungkus kesalahan dalam struct, Anda tidak dapat memberikan konteks tambahan.

Katakanlah proposal umum yang diterima memungkinkan Anda mendefinisikan sesuatu seperti berikut (sintaks arbitrer yang saya coba buat dengan sengaja jelek sehingga tidak ada yang akan fokus padanya)

type MyError generic_over[E which_is_a_type_satisfying error] struct {
  E
  extraContext extraContextType
}
func (m MyError) Error() string {
  return fmt.Sprintf("%s: %s", m.extraContext, m.E)
}

Dengan memanfaatkan penyematan, kami dapat menyematkan jenis beton apa pun yang memenuhi antarmuka kesalahan dan keduanya membungkusnya dan memiliki akses ke metode lainnya. Sayangnya ini hanya membuat kita setengah jalan ke sana.

Yang benar-benar kita butuhkan di sini adalah mengambil nilai sembarang dari antarmuka kesalahan dan menyematkan tipe dinamisnya.

Ini segera menimbulkan dua kekhawatiran

  • tipenya harus dibuat saat runtime (mungkin diperlukan oleh refleksi)
  • pembuatan tipe harus panik jika nilai kesalahannya nihil

Jika itu belum membuat Anda berpikir, Anda juga memerlukan mekanisme untuk "melompati" antarmuka ke tipe dinamisnya, baik dengan anotasi dalam daftar parameter generik untuk mengatakan "selalu instantiate pada tipe dinamis dari nilai antarmuka " atau dengan beberapa fungsi ajaib yang hanya dapat dipanggil selama instantiasi tipe untuk membuka kotak antarmuka sehingga jenis dan nilainya dapat disambungkan dengan benar.

Tanpa itu, Anda hanya membuat instance MyError pada jenis kesalahan itu sendiri, bukan pada jenis antarmuka yang dinamis.

Katakanlah kita memiliki fungsi ajaib unbox untuk ditarik dan (entah bagaimana) menerapkan informasi:

func wrap(ec extraContext, err error) error {
  if err == nil {
    return nil
  }
  return MyError{
    E: unbox(err),
    extraContext: ec,
  }
}

Sekarang katakanlah kita memiliki kesalahan non-nil, err , yang tipe dinamisnya adalah *net.DNSError . Lalu ini

wrapped := wrap(getExtraContext(), err)
//wrapped 's dynamic type is a MyStruct embedding E=*net.DNSError
_, ok := wrapped.(net.Error)
fmt.Println(ok)

akan mencetak true . Tetapi jika tipe dinamis dari err adalah *os.PathError itu akan dicetak salah.

Saya harap semantik yang diusulkan jelas mengingat sintaks tumpul yang digunakan dalam demonstrasi.

Saya juga berharap ada cara yang lebih baik untuk menyelesaikan masalah itu dengan mekanisme dan upacara yang lebih sedikit, tetapi saya pikir cara di atas bisa berhasil.

@jimmyfrasche Jika saya mengerti apa yang Anda inginkan, ini adalah mekanisme adaptasi tanpa pembungkus. Anda ingin dapat memperluas kumpulan operasi yang ditawarkan suatu tipe tanpa membungkusnya dengan objek lain yang menyembunyikan aslinya. Ini adalah fungsi yang ditawarkan Genus.

@andrewcmyers no.

Struct di Go memungkinkan penyematan. Jika Anda menambahkan bidang tanpa nama tetapi dengan tipe ke struct, ia melakukan dua hal: Ini membuat bidang dengan nama yang sama dengan jenisnya dan memungkinkan pengiriman transparan ke metode apa pun dari jenis itu. Kedengarannya sangat seperti warisan tetapi tidak. Jika Anda memiliki tipe T yang memiliki metode Foo() maka berikut ini setara

type S struct {
  T
}

dan

type S struct {
  T T
}
func (s S) Foo() {
  s.T.Foo()
}

(ketika Foo disebut "ini" selalu bertipe T).

Anda juga dapat menyematkan antarmuka dalam struct. Ini memberikan struct semua metode dalam kontrak antarmuka (meskipun Anda perlu menetapkan beberapa nilai dinamis ke bidang implisit atau itu akan menyebabkan kepanikan dengan pengecualian pointer nol yang setara)

Go memiliki antarmuka yang mendefinisikan kontrak dalam istilah metode tipe. Nilai jenis apa pun yang memenuhi kontrak dapat dimasukkan ke dalam nilai antarmuka itu. Nilai antarmuka adalah penunjuk ke manifes tipe internal (tipe dinamis) dan penunjuk ke nilai tipe dinamis itu (nilai dinamis). Anda dapat melakukan jenis pernyataan pada nilai antarmuka untuk (a) mendapatkan nilai dinamis jika Anda menegaskan ke jenis non-antarmuka atau (b) mendapatkan nilai antarmuka baru jika Anda menegaskan ke antarmuka yang berbeda bahwa nilai dinamis juga memenuhi. Itu umum untuk menggunakan yang terakhir untuk "menguji fitur" suatu objek untuk melihat apakah itu mendukung metode opsional. Untuk menggunakan kembali contoh sebelumnya, beberapa kesalahan memiliki metode "Temporary() bool" sehingga Anda dapat melihat apakah ada kesalahan yang bersifat sementara dengan:

func isTemp(err error) bool {
  if t, ok := err.(interface{ Temporary() bool}); ok {
    return t.Temporary()
  }
  return false
}

Itu juga umum untuk membungkus jenis dalam jenis lain untuk menyediakan fitur tambahan. Ini berfungsi baik dengan tipe non-antarmuka. Saat Anda membungkus antarmuka, Anda juga menyembunyikan metode yang tidak Anda ketahui dan Anda tidak dapat memulihkannya dengan pernyataan tipe "uji fitur": tipe yang dibungkus hanya memperlihatkan metode antarmuka yang diperlukan meskipun memiliki metode opsional . Mempertimbangkan:

type A struct {}
func (A) Foo()
func (A) Bar()

type I interface {
  Foo()
}

type B struct {
  I
}

var i I = B{A{}}

Anda tidak dapat memanggil Bar pada i atau bahkan mengetahui bahwa itu ada kecuali Anda tahu bahwa tipe dinamis saya adalah B sehingga Anda dapat membukanya dan membuka bidang I untuk mengetikkan pernyataan itu .

Ini menyebabkan masalah nyata, terutama berurusan dengan antarmuka umum seperti kesalahan, atau Pembaca.

Jika ada cara untuk mengangkat tipe dan nilai dinamis dari antarmuka (dalam beberapa cara yang aman dan terkendali), Anda dapat membuat parameter tipe baru dengan itu, menyetel bidang yang disematkan ke nilai, dan mengembalikan antarmuka baru. Kemudian Anda mendapatkan nilai yang memenuhi antarmuka asli, memiliki fungsionalitas yang ditingkatkan yang ingin Anda tambahkan, tetapi sisa metode dari tipe dinamis asli masih ada untuk diuji fitur.

@jimmyfrasche Memang. Apa Genus memungkinkan Anda lakukan adalah menggunakan satu jenis untuk memenuhi kontrak "antarmuka" tanpa tinju itu. Nilai masih memiliki tipe aslinya dan operasi aslinya. Lebih lanjut, program dapat menentukan operasi mana yang harus digunakan tipe untuk memenuhi kontrak -- secara default, ini adalah operasi yang disediakan tipe, tetapi program dapat menyediakan operasi baru jika tipe tidak memiliki operasi yang diperlukan. Itu juga dapat menggantikan operasi yang akan digunakan oleh tipe tersebut.

@jimmyfrasche @andrewcmyers Untuk kasus penggunaan itu, lihat juga https://github.com/golang/go/issues/4146#issuecomment -318200547.

@jimmyfrasche Bagi saya, sepertinya masalah utama di sini adalah mendapatkan tipe/nilai dinamis dari suatu variabel. Mengesampingkan embedding, contoh yang disederhanakan adalah

type MyError generic_over[E which_is_a_type_satisfying error] struct {
  e E
  extraContext extraContextType
}
func (m MyError) Error() string {
  return fmt.Sprintf("%s: %s", m.extraContext, m.e)
}

Nilai yang ditetapkan ke e harus memiliki tipe dinamis (atau konkret) seperti *net.DNSError , yang mengimplementasikan error . Berikut adalah beberapa kemungkinan cara perubahan bahasa di masa mendatang dapat mengatasi masalah ini:

  1. Memiliki fungsi ajaib unbox -seperti yang mengungkap nilai dinamis variabel. Ini berlaku untuk semua jenis yang tidak konkret, misalnya serikat pekerja.
  2. Jika perubahan bahasa mendukung variabel tipe, sediakan sarana untuk mendapatkan tipe dinamis dari variabel. Dengan informasi tipe, kita dapat menulis sendiri fungsi unbox . Sebagai contoh,
func unbox(v T1) T2 {
    t := dynTypeOf(v)
    return v.(t)
}

wrap dapat ditulis dengan cara yang sama seperti sebelumnya, atau sebagai

func wrap(ec extraContext, err error) error {
  if err == nil {
    return nil
  }
  t := dynTypeOf(err)
  return MyError{
    e: v.(t),
    extraContext: ec,
  }
}
  1. Jika perubahan bahasa mendukung batasan tipe, berikut adalah ide alternatif:
type E1 which_is_a_type_satisfying error
type E2 which_is_a_type_satisfying error

func wrap(ec extraContext, err E1) E2 {
  if err == nil {
    return nil
  }
  return MyError{
    e: err,
    extraContext: ec,
  }
}

Dalam contoh ini, kami menerima nilai jenis apa pun yang mengimplementasikan kesalahan. Setiap pengguna wrap yang mengharapkan error akan menerimanya. Namun, jenis e di dalam MyError sama dengan err yang diteruskan, yang tidak terbatas pada jenis antarmuka. Jika seseorang menginginkan perilaku yang sama dengan 2,

var iface error = ...
wrap(getExtraContext(), unbox(iface))

Karena tampaknya tidak ada orang lain yang melakukannya, saya ingin menunjukkan "laporan pengalaman" yang sangat jelas untuk obat generik seperti yang diminta oleh https://blog.golang.org/toward-go2.

Yang pertama adalah tipe map bawaan:

m := make(map[string]string)

Berikutnya adalah tipe chan bawaan:

c := make(chan bool)

Akhirnya, perpustakaan standar dipenuhi dengan alternatif interface{} di mana obat generik akan bekerja lebih aman:

  • heap.Interface (https://golang.org/pkg/container/heap/#Interface)
  • list.Element (https://golang.org/pkg/container/list/#Element)
  • ring.Ring (https://golang.org/pkg/container/ring/#Ring)
  • sync.Pool (https://golang.org/pkg/sync/#Pool)
  • sync.Map mendatang (https://tip.golang.org/pkg/sync/#Map)
  • atomic.Value (https://golang.org/pkg/sync/atomic/#Value)

Mungkin ada orang lain yang saya lewatkan. Intinya, masing-masing di atas adalah di mana saya berharap obat generik berguna.

(Catatan: Saya tidak menyertakan sort.Sort di sini karena ini adalah contoh yang sangat baik tentang bagaimana antarmuka dapat digunakan daripada generik.)

http://www.yinwang.org/blog-cn/2014/04/18/golang
Saya pikir generik itu penting. Jika tidak, tidak dapat menangani tipe yang serupa. Kadang-kadang antarmuka tidak dapat menyelesaikan masalah.

Sintaks sederhana dan sistem tipe adalah kelebihan penting dari Go. Jika Anda menambahkan obat generik, bahasa akan menjadi berantakan seperti Scala atau Haskell. Juga fitur ini akan menarik fanboy pseudo-akademik, yang pada akhirnya akan mengubah nilai-nilai komunitas dari "Ayo selesaikan ini" menjadi "Ayo bicara tentang teori dan matematika CS". Hindari obat generik, ini adalah jalan menuju jurang maut.

@bxqgit tolong jaga kesopanan ini. Tidak perlu menghina siapa pun.

Adapun apa yang akan terjadi di masa depan, kita akan lihat, tetapi saya tahu bahwa selama 98% waktu saya, saya tidak membutuhkan obat generik, kapan pun saya membutuhkannya, saya berharap dapat menggunakannya. Bagaimana mereka digunakan vs bagaimana mereka digunakan secara salah adalah diskusi yang berbeda. Mendidik pengguna harus menjadi bagian dari proses.

@bxqgit
Ada situasi di mana generik diperlukan, seperti struktur data generik (Pohon, Tumpukan, Antrian, ...) atau fungsi generik (Peta, Filter, Kurangi, ...) dan hal-hal ini tidak dapat dihindari, menggunakan antarmuka alih-alih generik dalam situasi ini hanya menambah kompleksitas besar baik untuk penulis kode dan pembaca kode dan juga memiliki efek buruk pada efisiensi kode saat run-time sehingga harus jauh lebih rasional untuk menambahkan ke bahasa generik daripada mencoba menggunakan antarmuka dan mencerminkan untuk menulis kompleks dan kode yang tidak efisien.

@bxqgit Menambahkan obat generik tidak selalu menambah kompleksitas bahasa, ini dapat dicapai dengan sintaks sederhana juga. Dengan obat generik, Anda menambahkan batasan tipe waktu kompilasi variabel yang sangat berguna dengan struktur data, seperti yang dikatakan @riwogo .

Sistem antarmuka saat ini di go sangat berguna, namun sangat buruk ketika Anda membutuhkan, misalnya, implementasi umum daftar, yang dengan antarmuka memerlukan batasan tipe waktu eksekusi, namun jika Anda menambahkan generik, tipe generik dapat diganti di kompilasi waktu dengan tipe aktual, membuat batasan tidak perlu.

Juga, ingat, orang-orang di belakang pergi, kembangkan bahasa menggunakan apa yang Anda sebut "teori dan matematika CS", dan juga orang-orang yang "menyelesaikan ini".

Juga, ingat, orang-orang di belakang pergi, kembangkan bahasa menggunakan apa yang Anda sebut "teori dan matematika CS", dan juga orang-orang yang "menyelesaikan ini".

Secara pribadi saya tidak melihat banyak teori CS dan matematika dalam desain bahasa Go. Ini adalah bahasa yang cukup primitif, yang menurut saya bagus. Juga orang-orang yang Anda bicarakan memutuskan untuk menghindari obat generik dan menyelesaikan sesuatu. Jika bekerja dengan baik, mengapa mengubah sesuatu? Secara umum, saya berpikir bahwa terus-menerus mengembangkan dan memperluas sintaksis bahasa adalah praktik yang buruk. Itu hanya menambah kerumitan yang mengarah pada kekacauan Haskell dan Scala.

Templatenya rumit tetapi Generik itu sederhana

Lihat fungsi SortInts, SortFloats, SortStrings dalam paket sortir. Atau SearchInts, SearchFloats, SearchStrings. Atau metode Len, Less, dan Swap dari byName dalam paket io/ioutil. Penyalinan boilerplate murni.

Fungsi salin dan tambahkan ada karena membuat irisan jauh lebih berguna. Generik berarti bahwa fungsi-fungsi ini tidak diperlukan. Generik akan memungkinkan untuk menulis fungsi serupa untuk peta dan saluran, belum lagi tipe data yang dibuat pengguna. Memang, irisan adalah tipe data komposit yang paling penting, dan itulah mengapa fungsi ini diperlukan, tetapi tipe data lain masih berguna.

Pilihan saya adalah tidak untuk aplikasi generik yang digeneralisasi, ya untuk lebih banyak fungsi generik bawaan seperti append dan copy yang bekerja pada beberapa tipe dasar. Mungkin sort dan search dapat ditambahkan untuk jenis koleksi?

Untuk aplikasi saya, satu-satunya tipe yang hilang adalah kumpulan yang tidak berurutan (https://github.com/golang/go/issues/7088), saya ingin ini sebagai tipe bawaan sehingga mendapatkan pengetikan umum seperti slice dan map . Masukkan pekerjaan ke dalam kompilator (pembandingan untuk setiap tipe dasar dan set yang dipilih dari tipe struct kemudian atur untuk kinerja terbaik) dan jauhkan anotasi tambahan dari kode aplikasi.

smap built-in bukannya sync.Map juga tolong. Dari pengalaman saya menggunakan interface{} untuk keamanan tipe runtime adalah cacat desain. Pengecekan tipe waktu kompilasi adalah alasan utama untuk menggunakan Go.

@pciet

Dari pengalaman saya menggunakan antarmuka{} untuk keamanan tipe waktu proses adalah cacat desain.

Bisakah Anda menulis pembungkus kecil (jenis brankas)?
https://play.golang.org/p/tG6hd-j5yx

@pierrre Pembungkus itu lebih baik daripada cek reflect.TypeOf(item).AssignableTo(type) . Tetapi menulis tipe Anda sendiri dengan map + sync.Mutex atau sync.RWMutex adalah kerumitan yang sama tanpa pernyataan tipe yang diperlukan sync.Map .

Penggunaan peta saya yang disinkronkan adalah untuk peta global mutex dengan var myMapLock = sync.RWMutex{} di sebelahnya alih-alih membuat tipe. Ini bisa lebih bersih. Tipe bawaan yang umum terdengar tepat bagi saya tetapi membutuhkan pekerjaan yang tidak dapat saya lakukan, dan saya lebih suka pendekatan saya daripada tipe yang menegaskan.

Saya menduga bahwa reaksi negatif yang mendalam terhadap obat generik yang tampaknya muncul oleh banyak programmer Go karena paparan utama mereka terhadap obat generik adalah melalui template C++. Ini sangat disayangkan karena C++ mendapatkan generik yang salah secara tragis sejak hari pertama dan telah menambah kesalahan sejak itu. Generik untuk Go bisa menjadi jauh lebih sederhana dan tidak rawan kesalahan.

Akan mengecewakan melihat Go menjadi semakin kompleks dengan menambahkan tipe parameter bawaan. Akan lebih baik hanya menambahkan dukungan bahasa untuk programmer untuk menulis tipe parameter mereka sendiri. Kemudian tipe khusus hanya bisa disediakan sebagai perpustakaan daripada mengacaukan bahasa inti.

@andrewcmyers "Generik untuk Go bisa menjadi jauh lebih sederhana dan tidak terlalu rawan kesalahan." --- seperti obat generik di C#.

Sangat mengecewakan melihat Go menjadi semakin kompleks dengan menambahkan tipe parameter bawaan.

Terlepas dari spekulasi dalam masalah ini, saya pikir ini sangat tidak mungkin terjadi.

Eksponen pada ukuran kompleksitas tipe parameter adalah varians.
Tipe Go (kecuali antarmuka) tidak berubah dan ini dapat dan harus
menjaga aturan.

Implementasi generik "type copy-paster" mekanis, dibantu kompiler
akan memecahkan 99% masalah dengan cara yang sesuai dengan dasar Go
prinsip kedangkalan dan tidak mengejutkan.

Omong-omong, ini dan lusinan ide layak lainnya telah dibahas
sebelumnya dan beberapa bahkan mencapai puncaknya dengan pendekatan yang baik dan bisa diterapkan. Pada ini
intinya, saya sangat membenci kertas timah tentang bagaimana mereka semua menghilang
diam-diam ke dalam kehampaan.

Pada 28 November 2017 23:54, "Andrew Myers" [email protected] menulis:

Saya menduga bahwa reaksi mendalam negatif terhadap obat generik yang banyak Go
programmer tampaknya muncul karena paparan utama mereka terhadap obat generik adalah
melalui template C++. Ini sangat disayangkan karena C++ mendapatkan obat generik secara tragis
salah dari hari 1 dan telah memperparah kesalahan sejak itu. Obat generik untuk
Go bisa menjadi jauh lebih sederhana dan tidak rawan kesalahan.

Sangat mengecewakan melihat Go menjadi semakin kompleks dengan menambahkan
tipe parameter bawaan. Akan lebih baik hanya menambahkan bahasa
dukungan bagi programmer untuk menulis tipe parameter mereka sendiri. Kemudian
tipe khusus hanya bisa disediakan sebagai perpustakaan daripada berantakan
bahasa inti.


Anda menerima ini karena Anda disebutkan.
Balas email ini secara langsung, lihat di GitHub
https://github.com/golang/go/issues/15292#issuecomment-347691444 , atau bisukan
benang
https://github.com/notifications/unsubscribe-auth/AJZ_jPsQd2qBbn9NI1wZeT-O2JpyraTMks5s7I81gaJpZM4IG-xv
.

Ya, Anda dapat memiliki obat generik tanpa template. Template adalah bentuk polimorfisme parametrik canggih sebagian besar untuk fasilitas metaprogramming.

@ianlancetaylor Rust memungkinkan program untuk mengimplementasikan suatu sifat T pada tipe yang ada Q , asalkan peti mereka mendefinisikan T atau Q .

Hanya sebuah pemikiran: Saya ingin tahu apakah Simon Peyton Jones (ya, dari ketenaran Haskell) dan/atau pengembang Rust mungkin dapat membantu. Rust dan Haskell mungkin memiliki dua sistem tipe paling canggih dari semua bahasa produksi, dan Go harus belajar dari mereka.

Ada juga Phillip Wadler , yang mengerjakan Generic Java , yang akhirnya mengarah pada implementasi generik yang dimiliki Java saat ini.

@tarcieri Saya tidak berpikir bahwa obat generik Java sangat bagus, tetapi mereka telah teruji dalam pertempuran.

@DemiMarie Kami telah mengundang Andrew Myers di sini, untungnya.

Berdasarkan pengalaman pribadi saya, saya pikir orang yang tahu banyak tentang bahasa yang berbeda dan sistem tipe yang berbeda dapat sangat membantu dalam memeriksa ide. Tetapi untuk menghasilkan ide-ide di tempat pertama, yang kita butuhkan adalah orang-orang yang sangat akrab dengan Go, cara kerjanya hari ini, dan cara kerjanya yang wajar di masa depan. Go dirancang untuk menjadi, antara lain, bahasa yang sederhana. Mengimpor ide dari bahasa seperti Haskell atau Rust, yang jauh lebih rumit daripada Go, sepertinya tidak cocok. Dan secara umum ide-ide dari orang-orang yang belum pernah menulis kode Go dalam jumlah yang wajar sepertinya tidak cocok; bukan karena idenya akan buruk, hanya saja mereka tidak cocok dengan bahasa lainnya.

Misalnya, penting untuk dipahami bahwa Go sudah memiliki dukungan parsial untuk pemrograman generik menggunakan tipe antarmuka dan sudah (hampir) memiliki dukungan lengkap menggunakan paket refleksi. Sementara dua pendekatan untuk pemrograman generik tersebut tidak memuaskan karena berbagai alasan, setiap proposal untuk generik di Go harus berinteraksi dengan baik dengan keduanya sekaligus mengatasi kekurangannya.

Sebenarnya, ketika saya di sini, beberapa waktu lalu saya berpikir tentang pemrograman generik dengan antarmuka untuk sementara waktu, dan muncul dengan tiga alasan mengapa gagal memuaskan.

  1. Antarmuka membutuhkan semua operasi untuk dinyatakan sebagai metode. Itu membuatnya sulit untuk menulis antarmuka untuk tipe bawaan, seperti tipe saluran. Semua jenis saluran mendukung operator <- untuk operasi kirim dan terima, dan cukup mudah untuk menulis antarmuka dengan metode Send dan Receive , tetapi untuk menetapkan nilai saluran untuk tipe antarmuka itu Anda harus menulis metode pelat boilerplate Send dan Receive . Metode boilerplate tersebut akan terlihat persis sama untuk setiap jenis saluran yang berbeda, yang membosankan.

  2. Antarmuka diketik secara dinamis, sehingga kesalahan yang menggabungkan nilai yang diketik secara statis berbeda hanya ditangkap pada waktu berjalan, bukan waktu kompilasi. Misalnya, fungsi Merge yang menggabungkan dua saluran menjadi satu saluran menggunakan metode Send dan Receive akan mengharuskan kedua saluran memiliki elemen dengan tipe yang sama, tetapi pemeriksaan hanya dapat dilakukan pada saat run time.

  3. Antarmuka selalu kotak. Misalnya, tidak ada cara untuk menggunakan antarmuka untuk menggabungkan sepasang tipe lain tanpa menempatkan tipe lain tersebut ke dalam nilai antarmuka, yang memerlukan alokasi memori tambahan dan pengejaran pointer.

Saya senang untuk mengoceh pada proposal generik untuk Go. Mungkin yang juga menarik adalah meningkatnya jumlah penelitian tentang obat generik di Cornell akhir-akhir ini, yang tampaknya relevan dengan apa yang mungkin dilakukan dengan Go:

http://www.cs.cornell.edu/andru/papers/familia/ (Zhang & Myers, OOPSLA'17)
http://io.livecode.ch/learn/namin/unsound (Amin & Tate, OOPSLA'16)
http://www.cs.cornell.edu/projects/genus/ (Zhang et al., PLDI '15)
https://www.cs.cornell.edu/~ross/publications/shapes/shapes-pldi14.pdf (Greenman, Muehlboeck & Tate, PLDI '14)

Dalam pembandingan peta vs. irisan untuk tipe set yang tidak berurutan, saya menulis unit test terpisah untuk masing-masing, tetapi dengan tipe antarmuka saya dapat menggabungkan dua daftar tes tersebut menjadi satu:

type Item interface {
    Equal(Item) bool
}

type Set interface {
    Add(Item) Set
    Remove(Item) Set
    Combine(...Set) Set
    Reduce() Set
    Has(Item) bool
    Equal(Set) bool
    Diff(Set) Set
}

Pengujian menghapus item:

type RemoveCase struct {
    Set
    Item
    Out Set
}

func TestRemove(t *testing.T) {
    for i, c := range RemoveCases {
        if c.Out.Equal(c.Set.Remove(c.Item)) == false {
            t.Fatalf("%v failed", i)
        }
    }
}

Dengan cara ini saya dapat menyatukan kasing saya yang sebelumnya terpisah menjadi satu irisan kasing tanpa masalah:

var RemoveCases = []RemoveCase{
    {
        Set: MapPathSet{
            &Path{{0, 0}}:         {},
            &Path{{0, 1}, {1, 1}}: {},
        },
        Item: Path{{0, 0}},
        Out: MapPathSet{
            &Path{{0, 1}, {1, 1}}: {},
        },
    },
    {
        Set: SlicePathSet{
            {{0, 0}},
            {{0, 1}, {1, 1}},
        },
        Item: Path{{0, 0}},
        Out: SlicePathSet{
            {{0, 1}, {1, 1}},
        },
    },
}

Untuk setiap tipe beton saya harus mendefinisikan metode antarmuka. Sebagai contoh:

func (the MapPathSet) Remove(an Item) Set {
    return MapDelete(the, an.(Path))
}
func (the SlicePathSet) Remove(an Item) Set {
    return SliceDelete(the, an.(Path))
}

Tes generik ini dapat menggunakan pemeriksaan tipe waktu kompilasi yang diusulkan:

type Item generic {
    Equal(Item) bool
}
func (the SlicePathSet) Remove(an Item) Set {
    return SliceDelete(the, an)
}

Sumber: https://github.com/pciet/pathsetbenchmark

Memikirkan hal itu lebih jauh, sepertinya pemeriksaan tipe waktu kompilasi tidak mungkin dilakukan untuk tes semacam itu karena Anda harus menjalankan program untuk mengetahui apakah suatu tipe diteruskan ke metode antarmuka yang sesuai.

Jadi bagaimana dengan tipe "generik" yang merupakan antarmuka dan memiliki pernyataan tipe tak terlihat yang ditambahkan oleh kompiler saat digunakan secara konkret?

@andrewcmyers Makalah "Familia" menarik (dan jauh di atas kepala saya). Gagasan kuncinya adalah warisan. Bagaimana konsep akan berubah untuk bahasa seperti Go yang mengandalkan komposisi alih-alih pewarisan?

Terima kasih. Bagian warisan tidak berlaku untuk Go -- jika Anda hanya tertarik pada obat generik untuk Go, Anda dapat berhenti membaca setelah bagian 4 makalah. Hal utama tentang makalah yang relevan dengan Go adalah bahwa makalah ini menunjukkan cara menggunakan antarmuka baik dalam cara mereka digunakan untuk Go sekarang maupun sebagai batasan pada tipe untuk abstraksi umum. Yang berarti Anda mendapatkan kekuatan kelas tipe Haskell tanpa menambahkan konstruksi yang sama sekali baru ke bahasa.

@andrewcmyers Bisakah Anda memberi contoh bagaimana tampilannya di Go?

Hal utama tentang makalah yang relevan dengan Go adalah bahwa makalah ini menunjukkan cara menggunakan antarmuka baik dalam cara mereka digunakan untuk Go sekarang maupun sebagai batasan pada tipe untuk abstraksi umum.

Pemahaman saya adalah bahwa antarmuka Go mendefinisikan batasan pada suatu tipe (misalnya "tipe ini dapat dibandingkan untuk kesetaraan menggunakan 'jenis antarmuka yang Sebanding' karena memenuhi metode Persamaan"). Saya tidak yakin saya mengerti apa yang Anda maksud dengan batasan tipe.

Saya tidak akrab dengan Haskell tetapi membaca tinjauan singkat membuat saya menebak bahwa tipe yang sesuai dengan antarmuka Go akan masuk ke dalam kelas tipe itu. Bisakah Anda menjelaskan apa yang berbeda tentang kelas tipe Haskell?

Perbandingan konkret antara Familia dan Go akan menarik. Terima kasih telah membagikan makalah Anda.

Antarmuka Go dapat dilihat sebagai penggambaran batasan pada tipe, melalui subtipe struktural. Namun, batasan tipe itu, sebagaimana adanya, tidak cukup ekspresif untuk menangkap batasan yang Anda inginkan untuk pemrograman generik. Misalnya, Anda tidak dapat mengungkapkan batasan tipe bernama Eq di makalah Familia.

Beberapa pemikiran tentang motivasi untuk fasilitas pemrograman yang lebih umum di Go:

Jadi ada daftar tes generik saya yang tidak benar-benar membutuhkan apa pun yang ditambahkan ke bahasa. Menurut pendapat saya bahwa tipe generik yang saya usulkan tidak memenuhi tujuan Go dari pemahaman langsung, itu tidak ada hubungannya dengan istilah pemrograman yang diterima secara umum, dan melakukan pernyataan tipe di sana tidak jelek karena kepanikan pada kegagalan adalah bagus. Saya puas dengan fasilitas pemrograman generik Go untuk kebutuhan saya.

Tapi sync.Map adalah kasus penggunaan yang berbeda. Ada kebutuhan di perpustakaan standar untuk implementasi peta tersinkronisasi generik yang matang di luar hanya struct dengan peta dan mutex. Untuk penanganan tipe, kita bisa membungkusnya dengan tipe lain yang menyetel tipe{} non-antarmuka dan melakukan penegasan tipe, atau kita bisa menambahkan pemeriksaan refleksi secara internal sehingga item yang mengikuti tipe pertama harus cocok dengan tipe yang sama. Keduanya memiliki pemeriksaan runtime, pembungkusnya memerlukan penulisan ulang setiap metode untuk setiap jenis penggunaan tetapi ia menambahkan pemeriksaan jenis waktu kompilasi untuk input dan menyembunyikan pernyataan tipe output, dan dengan pemeriksaan internal kita tetap harus melakukan pernyataan tipe output. Apa pun itu, kami melakukan konversi antarmuka tanpa penggunaan antarmuka yang sebenarnya; interface{} adalah peretasan bahasa dan tidak akan jelas bagi programmer Go baru. Meskipun json.Marshal adalah desain yang bagus menurut saya (termasuk tag struct yang jelek tapi masuk akal).

Saya akan menambahkan bahwa karena sync.Map ada di perpustakaan standar, idealnya itu harus menukar implementasi untuk kasus penggunaan yang diukur di mana struct sederhana lebih berkinerja. Peta yang tidak disinkronkan adalah jebakan awal yang umum dalam pemrograman bersamaan Go dan perbaikan pustaka standar seharusnya berfungsi.

Peta biasa hanya memiliki pemeriksaan tipe waktu kompilasi dan tidak memerlukan perancah ini. Saya berpendapat bahwa sync.Map harus sama atau tidak boleh ada di perpustakaan standar untuk Go 2.

Saya mengusulkan untuk menambahkan sync.Map ke daftar tipe bawaan dan melakukan hal yang sama untuk kebutuhan serupa di masa mendatang. Tetapi pemahaman saya adalah memberi programmer Go cara untuk melakukan ini tanpa harus bekerja pada kompiler dan melalui tantangan penerimaan open source adalah ide di balik diskusi ini. Dalam pandangan saya, memperbaiki sync.Map adalah kasus nyata yang mendefinisikan sebagian proposal generik ini.

Jika Anda menambahkan sync.Map sebagai built-in, lalu seberapa jauh Anda melangkah? Apakah Anda kasus khusus setiap wadah?
sync.Map bukan satu-satunya wadah dan beberapa lebih baik untuk beberapa kasus daripada yang lain.

@Azareal : @chowey mendaftarkan ini pada bulan Agustus:

Terakhir, pustaka standar dipenuhi dengan alternatif{} antarmuka di mana obat generik akan bekerja lebih aman:

• heap.Interface (https://golang.org/pkg/container/heap/#Interface)
• list.Element (https://golang.org/pkg/container/list/#Element)
• ring.Ring (https://golang.org/pkg/container/ring/#Ring)
• sync.Pool (https://golang.org/pkg/sync/#Pool)
• sync.Map yang akan datang (https://tip.golang.org/pkg/sync/#Map)
• atomic.Value (https://golang.org/pkg/sync/atomic/#Value)

Mungkin ada orang lain yang saya lewatkan. Intinya, masing-masing di atas adalah di mana saya berharap obat generik berguna.

Dan saya ingin set yang tidak berurutan untuk tipe yang dapat dibandingkan untuk kesetaraan.

Saya ingin banyak pekerjaan dimasukkan ke dalam implementasi variabel di runtime untuk setiap jenis berdasarkan pembandingan sehingga implementasi terbaik biasanya yang digunakan.

Saya ingin tahu apakah ada implementasi alternatif yang masuk akal dengan Go 1 yang mencapai tujuan yang sama untuk tipe pustaka standar ini tanpa antarmuka{} dan tanpa generik.

antarmuka golang dan kelas tipe haskell mengatasi dua hal (yang sangat hebat!):

1.) (Jenis Kendala) Mereka mengelompokkan jenis yang berbeda dengan satu tag, nama antarmuka
2.) (Pengiriman) Mereka menawarkan untuk mengirim secara berbeda pada setiap jenis untuk serangkaian fungsi tertentu melalui implementasi antarmuka

Tetapi,

1.) Terkadang Anda hanya menginginkan grup anonim seperti grup int, float64, dan string. Bagaimana Anda memberi nama antarmuka seperti itu, NumericandString?

2.) Sangat sering, Anda tidak ingin mengirimkan secara berbeda untuk setiap jenis antarmuka tetapi hanya menyediakan satu metode untuk semua jenis antarmuka yang terdaftar (Mungkin mungkin dengan metode antarmuka default )

3.) Sangat sering, Anda tidak ingin menghitung semua jenis yang mungkin untuk suatu grup. Alih-alih, Anda pergi dengan cara yang malas dan mengatakan saya ingin semua tipe T mengimplementasikan beberapa Antarmuka A dan kompiler kemudian mencari semua jenis di semua file sumber yang Anda edit dan di semua perpustakaan yang Anda gunakan untuk menghasilkan fungsi yang sesuai pada waktu kompilasi.

Meskipun poin terakhir dimungkinkan melalui polimorfisme antarmuka, ini memiliki kelemahan menjadi polimorfisme runtime yang melibatkan gips dan bagaimana Anda membatasi input parameter suatu fungsi untuk memuat tipe yang mengimplementasikan lebih dari satu antarmuka atau salah satu dari banyak antarmuka. Cara yang baik adalah dengan memperkenalkan antarmuka baru yang memperluas antarmuka lain (dengan antarmuka bersarang) untuk mencapai sesuatu yang serupa tetapi tidak dengan praktik terbaik.

Ngomong-ngomong.
Saya mengakui kepada mereka yang mengatakan bahwa go sudah memiliki polimorfisme dan oleh karena itu go bukan lagi bahasa sederhana seperti C. Ini adalah bahasa pemrograman sistem tingkat tinggi. Jadi mengapa tidak memperluas penawaran polimorfisme.

Inilah perpustakaan yang saya mulai hari ini untuk jenis kumpulan umum yang tidak berurutan: https://github.com/pciet/unordered

Ini memberikan contoh dokumentasi dan pengujian yang mengetik pola pembungkus (terima kasih @pierrre) untuk keamanan tipe waktu kompilasi dan juga memiliki pemeriksaan refleksi untuk keamanan tipe run-time.

Apa kebutuhan yang ada untuk obat generik? Sikap negatif saya terhadap tipe generik pustaka standar sebelumnya berpusat pada penggunaan antarmuka{}; keluhan saya dapat diselesaikan dengan tipe khusus paket untuk antarmuka{} (seperti type Item interface{} dalam pciet/unordered) yang mendokumentasikan batasan yang tidak dapat diungkapkan yang dimaksud.

Saya tidak melihat perlunya fitur bahasa tambahan ketika hanya dokumentasi yang bisa membawa kita ke sana sekarang. Sudah ada sejumlah besar kode yang teruji pertempuran di perpustakaan standar yang menyediakan fasilitas umum (lihat https://github.com/golang/go/issues/23077).

Jenis kode Anda diperiksa saat runtime (dan dari perspektif itu sama sekali tidak lebih baik daripada hanya interface{} jika tidak lebih buruk). Dengan obat generik Anda bisa memiliki tipe koleksi dengan pemeriksaan tipe waktu kompilasi.

@zerkms pemeriksaan run-time dapat dimatikan dengan mengatur asserting = false (ini tidak akan masuk ke perpustakaan standar), ada pola penggunaan untuk pemeriksaan waktu kompilasi, dan lagi pula pemeriksaan tipe hanya melihat struct antarmuka (menggunakan antarmuka menambahkan lebih banyak biaya daripada pemeriksaan jenis). Jika antarmuka tidak berfungsi maka Anda harus menulis tipe Anda sendiri.

Anda mengatakan kode generik kinerja maksimal adalah kebutuhan utama. Belum untuk kasus penggunaan saya, tetapi mungkin perpustakaan standar bisa menjadi lebih cepat, dan mungkin orang lain membutuhkan hal seperti itu.

pemeriksaan run-time dapat dimatikan dengan mengatur pernyataan = false

maka tidak ada yang menjamin kebenaran

Anda mengatakan kode generik kinerja maksimal adalah kebutuhan utama.

Saya tidak mengatakan itu. Keamanan jenis akan sangat membantu. Solusi Anda masih interface{} -terinfeksi.

tapi mungkin perpustakaan standar bisa menjadi lebih cepat, dan mungkin orang lain membutuhkan hal seperti itu.

mungkin, jika tim pengembang inti dengan senang hati mengimplementasikan apa pun yang saya butuhkan sesuai permintaan dan dengan cepat.

@pciet

Saya tidak melihat perlunya fitur bahasa tambahan ketika hanya dokumentasi yang bisa membawa kita ke sana sekarang.

Anda mengatakan ini, namun Anda tidak memiliki masalah menggunakan fitur bahasa generik dalam bentuk irisan dan fungsi make.

Saya tidak melihat perlunya fitur bahasa tambahan ketika hanya dokumentasi yang bisa membawa kita ke sana sekarang.

Lalu mengapa repot-repot menggunakan bahasa yang diketik secara statis? Anda dapat menggunakan bahasa yang diketik secara dinamis seperti Python dan mengandalkan dokumentasi untuk memastikan tipe data yang benar dikirim ke API Anda.

Saya pikir salah satu keuntungan dari Go adalah fasilitas untuk memberlakukan beberapa kendala oleh kompiler untuk mencegah bug di masa depan. Fasilitas tersebut dapat diperluas (dengan dukungan generik) untuk menerapkan beberapa kendala lain untuk mencegah beberapa bug lagi di masa mendatang.

Anda mengatakan ini, namun Anda tidak memiliki masalah menggunakan fitur bahasa generik dalam bentuk irisan dan fungsi make.

Saya mengatakan fitur yang ada membawa kita ke titik keseimbangan yang baik yang memang memiliki solusi pemrograman generik dan harus ada alasan nyata yang kuat untuk berubah dari sistem tipe Go 1. Bukan bagaimana perubahan akan meningkatkan bahasa tetapi masalah apa yang dihadapi orang sekarang, seperti mempertahankan banyak pergantian tipe waktu proses untuk antarmuka{} dalam paket pustaka standar fmt dan database, yang akan diperbaiki.

Lalu mengapa repot-repot menggunakan bahasa yang diketik secara statis? Anda dapat menggunakan bahasa yang diketik secara dinamis seperti Python dan mengandalkan dokumentasi untuk memastikan tipe data yang benar dikirim ke API Anda.

Saya telah mendengar saran untuk menulis sistem dengan Python alih-alih bahasa dan organisasi yang diketik secara statis.

Kebanyakan programmer Go yang menggunakan library standar menggunakan tipe yang tidak dapat dijelaskan secara lengkap tanpa dokumentasi atau tanpa melihat implementasinya. Tipe dengan subtipe parametrik atau tipe umum dengan batasan yang diterapkan hanya memperbaiki subset dari kasus ini secara terprogram dan akan menghasilkan banyak pekerjaan yang sudah dilakukan di pustaka standar.

Dalam proposal untuk tipe sum, saya menyarankan fitur build untuk sakelar tipe antarmuka di mana antarmuka yang digunakan dalam suatu fungsi atau metode memiliki kesalahan build yang dipancarkan ketika nilai yang mungkin ditetapkan ke antarmuka tidak cocok dengan kasus sakelar tipe antarmuka apa pun yang terkandung.

Fungsi/metode yang mengambil antarmuka dapat menolak beberapa tipe saat dibangun dengan tidak memiliki case default dan tidak ada case untuk tipe tersebut. Ini sepertinya tambahan pemrograman generik yang masuk akal jika fitur tersebut layak untuk diterapkan.

Jika antarmuka Go dapat menangkap tipe pelaksana, mungkin ada bentuk generik yang sepenuhnya kompatibel dengan sintaks Go saat ini - bentuk parameter tunggal generik ( demonstrasi ).

@dc0d untuk tipe wadah generik Saya percaya fitur itu menambahkan pemeriksaan tipe waktu kompilasi tanpa memerlukan tipe pembungkus: https://Gist.github.com/pciet/36a9dcbe99f6fb71f5fc2d3c455971e5

@pciet Anda benar. Dalam kode yang disediakan, No. 4, sampel menyatakan bahwa tipe ditangkap untuk irisan dan saluran (dan larik). Tapi tidak untuk peta, karena hanya ada satu dan hanya satu jenis parameter: pelaksana. Dan karena peta membutuhkan dua parameter tipe, antarmuka pembungkus diperlukan.

BTW saya harus menekankan tujuan demonstrasi kode itu, sebagai garis pemikiran. Saya bukan perancang bahasa. Ini hanyalah cara berpikir hipotetis tentang implementasi obat generik di Go:

  • Kompatibel dengan Go
  • Sederhana (parameter tipe generik tunggal, yang _terasa_ seperti _ini_ di OO lain, mengacu pada pelaksana saat ini)

Diskusi tentang generikitas dan semua kasus penggunaan yang mungkin dalam konteks keinginan untuk meminimalkan dampak sambil memaksimalkan kasus penggunaan yang penting dan fleksibilitas ekspresi adalah analisis yang sangat kompleks. Tidak yakin apakah ada di antara kita yang dapat menyaringnya menjadi prinsip-prinsip singkat alias esensi generatif. Saya mencoba. Bagaimanapun, inilah beberapa pemikiran awal saya dari _cursory_ saya membaca utas ini…

@adg menulis:

Mendampingi masalah ini adalah proposal generik umum oleh @ianlancetaylor yang mencakup empat proposal cacat spesifik dari mekanisme pemrograman generik untuk Go.

Afaics, bagian tertaut yang dikutip sebagai berikut gagal untuk menyatakan kasus generik yang kurang dengan antarmuka saat ini, _“Tidak ada cara untuk menulis metode yang mengambil antarmuka untuk tipe T yang disediakan pemanggil, untuk T apa pun, dan mengembalikan nilai tipe T yang sama.”_.

Tidak ada cara untuk menulis antarmuka dengan metode yang mengambil argumen tipe T, untuk T apa pun, dan mengembalikan nilai dengan tipe yang sama.

Jadi bagaimana lagi kode di jenis situs panggilan memeriksa apakah ia memiliki tipe T sebagai nilai hasil? Misalnya, antarmuka tersebut mungkin memiliki metode pabrik untuk membangun tipe T. Inilah mengapa kita perlu memparametrikan antarmuka pada tipe T.

Antarmuka bukan sekadar tipe; mereka juga nilai. Tidak ada cara untuk menggunakan tipe antarmuka tanpa menggunakan nilai antarmuka, dan nilai antarmuka tidak selalu efisien.

Setuju bahwa karena antarmuka saat ini tidak dapat diparametrikan secara eksplisit pada tipe T yang dioperasikannya, tipe T tidak dapat diakses oleh programmer.

Jadi, inilah yang dilakukan oleh batas kelas tipe di situs definisi fungsi yang mengambil sebagai input tipe T dan memiliki klausa where atau requires yang menyatakan antarmuka yang diperlukan untuk tipe T. Dalam banyak keadaan kamus antarmuka ini dapat secara otomatis dimonomorfisasi pada waktu kompilasi sehingga tidak ada penunjuk kamus (untuk antarmuka) yang diteruskan ke fungsi saat runtime (monomorfisasi yang saya anggap kompiler Go berlaku untuk antarmuka saat ini?). Dengan 'nilai' dalam kutipan di atas, saya kira yang dia maksud adalah tipe input T dan bukan kamus metode untuk tipe antarmuka yang diimplementasikan oleh tipe T.

Jika kemudian kita mengizinkan parameter tipe pada tipe data (misalnya struct ), maka tipe T di atas dapat diparameterisasi sendiri sehingga kita benar-benar memiliki tipe T<U> . Pabrik untuk tipe seperti itu yang perlu mempertahankan pengetahuan tentang U disebut tipe tipe yang lebih tinggi (HKT) .

Generik mengizinkan kontainer polimorfik yang aman untuk tipe.

Lihat juga masalah wadah _heterogen_ yang dibahas di bawah ini. Jadi dengan polimorfik yang kami maksud adalah generikitas dari jenis nilai wadah (misalnya jenis elemen koleksi), namun ada juga masalah apakah kita dapat menempatkan lebih dari satu jenis nilai dalam wadah secara bersamaan sehingga menjadikannya heterogen.


@tamird menulis:

Persyaratan ini tampaknya mengecualikan misalnya sistem yang mirip dengan sistem sifat Rust, di mana tipe generik dibatasi oleh batas sifat.

Batas sifat Rust pada dasarnya adalah batas kelas tipe.

@alex menulis:

Ciri-ciri karat. Sementara saya pikir mereka adalah model yang baik secara umum, mereka tidak cocok untuk Go seperti yang ada saat ini.

Mengapa menurut Anda mereka tidak cocok? Mungkin Anda memikirkan objek sifat yang menggunakan pengiriman runtime sehingga kurang berkinerja daripada monomorfisme? Tetapi itu dapat dipertimbangkan secara terpisah dari prinsip generikitas batas kelas tipe (lih diskusi saya tentang wadah/koleksi heterogen di bawah). Afaics, antarmuka Go sudah terikat sifat-seperti dan mencapai tujuan kelas tipe yang terlambat mengikat kamus ke tipe data di situs panggilan, daripada anti-pola OOP yang mengikat awal (bahkan jika masih di kompilasi- waktu) kamus ke tipe data (pada instantiasi/konstruksi). Kelas tipe dapat (setidaknya sebagian peningkatan derajat kebebasan) memecahkan Masalah Ekspresi yang tidak dapat dilakukan OOP.

@jimmyfrasche menulis:

  • https://golang.org/doc/faq#covariant_types

Saya setuju dengan tautan di atas bahwa kelas tipe memang tidak membuat subtipe dan tidak mengekspresikan hubungan pewarisan apa pun. Dan setuju dengan tidak perlu menggabungkan "kedermawanan" (sebagai konsep penggunaan kembali atau modularitas yang lebih umum daripada polimorfisme parametrik) dengan pewarisan seperti halnya subkelas.

Namun saya juga ingin menunjukkan bahwa hierarki pewarisan (alias subtipe) tidak dapat dihindari 1 pada penugasan ke (input fungsi) dan dari (keluaran fungsi) jika bahasa mendukung serikat pekerja dan persimpangan, karena misalnya int ν string dapat menerima tugas dari int atau string tetapi keduanya tidak dapat menerima tugas dari int ν string . Tanpa serikat pekerja, satu-satunya cara alternatif untuk menyediakan wadah/koleksi heterogen yang diketik secara statis adalah subkelas atau polimorfisme yang dibatasi secara eksistensial (alias objek sifat di Rust dan kuantifikasi eksistensial di Haskell). Tautan di atas berisi diskusi tentang kompromi antara eksistensial dan serikat pekerja. Afaik, satu-satunya cara untuk melakukan wadah/koleksi heterogen di Go sekarang adalah dengan memasukkan semua jenis ke interface{} kosong yang membuang informasi pengetikan dan akankah saya anggap memerlukan pemeriksaan tipe gips dan runtime, yang semacam 2 mengalahkan titik pengetikan statis.

"Anti-pola" yang harus dihindari adalah subkelas alias pewarisan virtual (lih juga "EDIT#2" tentang masalah dengan subsumsi implisit dan kesetaraan, dll).

1 Terlepas dari apakah mereka cocok secara struktural atau nominal karena subtipe disebabkan oleh Prinsip Substitusi Liskov berdasarkan set komparatif dan arah penugasan dengan input fungsi yang berlawanan dengan nilai yang dikembalikan, misalnya parameter tipe dari struct atau interface tidak dapat berada di kedua input fungsi dan mengembalikan nilai kecuali invarian alih-alih co- atau kontra-varian.

2 Absolutisme tidak akan berlaku karena kita tidak dapat mengetik periksa alam semesta non-determinisme tak terbatas. Jadi seperti yang saya pahami, utas ini adalah tentang memilih batas ("sweet spot") yang optimal untuk tingkat menyatakan mengetik wrt ke masalah generik.

@andrewcmyers menulis:

Tidak seperti generik Java dan C#, mekanisme generik Genus tidak didasarkan pada subtipe.

Ini adalah pewarisan dan subkelas ( bukan subtipe struktural ) yang merupakan anti-pola terburuk yang tidak ingin Anda salin dari Java, Scala, Ceylon, dan C++ (tidak terkait dengan masalah dengan template C++ ).

@thwd menulis:

Eksponen pada ukuran kompleksitas tipe parameter adalah varians. Tipe Go (kecuali antarmuka) bersifat invarian dan ini dapat dan harus tetap menjadi aturan.

Subtipe dengan kekekalan mengesampingkan kompleksitas kovarians. Kekekalan juga memperbaiki beberapa masalah dengan subkelas (misalnya Rectangle vs. Square ) tetapi tidak untuk yang lain (misalnya subsumsi implisit, kesetaraan, dll).

@bxqgit menulis:

Sintaks sederhana dan sistem tipe adalah kelebihan penting dari Go. Jika Anda menambahkan obat generik, bahasa akan menjadi berantakan seperti Scala atau Haskell.

Perhatikan bahwa Scala mencoba untuk menggabungkan OOP, subsclassing, FP, modul generik, HKT, dan kelas tipe (melalui implicit ) semuanya menjadi satu PL. Mungkin kelas tipe saja mungkin sudah cukup.

Haskell tidak selalu tumpul karena generik kelas tipe, tetapi lebih mungkin karena itu menegakkan fungsi murni di mana-mana dan menggunakan teori kategori monadik untuk memodelkan efek imperatif yang terkontrol.

Jadi saya pikir itu tidak benar untuk mengasosiasikan kebodohan dan kompleksitas PL tersebut dengan kelas tipe misalnya Rust. Dan jangan salahkan kelas tipe untuk masa hidup Rust + abstraksi peminjaman mutabilitas eksklusif.

Afaics, di bagian Semantik dari _Type Parameters di Go_, masalah yang dihadapi oleh @ianlancetaylor adalah masalah konseptualisasi karena dia (afaics) tampaknya tanpa disadari menciptakan kembali kelas tipe :

Bisakah kita menggabungkan SortableSlice dan PSortableSlice untuk mendapatkan yang terbaik dari kedua dunia? Tidak terlalu; tidak ada cara untuk menulis fungsi berparameter yang mendukung tipe dengan metode Less atau tipe bawaan. Masalahnya adalah SortableSlice.Less tidak dapat dipakai untuk suatu tipe tanpa metode Less , dan tidak ada cara untuk hanya membuat instance metode untuk beberapa tipe tetapi tidak untuk yang lain.

Klausa requires Less[T] untuk terikat kelas tipe (bahkan jika secara implisit disimpulkan oleh kompiler) pada metode Less untuk []T ada di T bukan []T . Implementasi kelas tipe Less[T] (yang berisi metode Less metode) untuk setiap T akan memberikan implementasi di badan fungsi metode atau menetapkan < fungsi bawaan sebagai implementasi. Namun saya percaya ini memerlukan HKT U[T] jika metode Sortable[U] memerlukan parameter tipe U yang mewakili tipe implementasi, misalnya []T . Afair @keean memiliki cara lain untuk menyusun semacam menggunakan kelas tipe terpisah untuk tipe nilai T yang tidak memerlukan HKT.

Perhatikan metode tersebut untuk []T mungkin mengimplementasikan kelas tipe Sortable[U] , di mana U adalah []T .

(Selain teknis: tampaknya kita dapat menggabungkan SortableSlice dan PSortableSlice dengan memiliki beberapa mekanisme untuk hanya membuat instance metode untuk beberapa jenis argumen tetapi tidak yang lain. Namun, hasilnya adalah mengorbankan kompilasi -keamanan tipe waktu, karena menggunakan tipe yang salah akan menyebabkan kepanikan runtime. Di Go, seseorang sudah dapat menggunakan tipe dan metode antarmuka dan tipe pernyataan untuk memilih perilaku saat runtime. Tidak perlu menyediakan cara lain untuk melakukan ini menggunakan parameter tipe .)

Pemilihan kelas tipe terikat di situs panggilan diselesaikan pada waktu kompilasi untuk T yang dikenal secara statis. Jika pengiriman dinamis heterogen diperlukan, lihat opsi yang saya jelaskan di posting saya sebelumnya.

Saya harap @keean dapat meluangkan waktu untuk datang ke sini dan membantu menjelaskan kelas tipe karena dia lebih ahli dan membantu saya mempelajari konsep-konsep ini. Saya mungkin memiliki beberapa kesalahan dalam penjelasan saya.

Catatan PS bagi mereka yang sudah membaca posting saya sebelumnya, perhatikan saya mengeditnya secara ekstensif sekitar 10 jam setelah mempostingnya (setelah tidur) semoga poin tentang wadah heterogen lebih koheren.


Bagian Siklus tampaknya salah. Konstruksi runtime dari instance S[T]{e} dari struct tidak ada hubungannya dengan pemilihan implementasi fungsi generik yang dipanggil. Dia mungkin berpikir bahwa kompiler tidak tahu apakah itu mengkhususkan implementasi fungsi generik untuk jenis argumen, tetapi semua jenis itu diketahui pada waktu kompilasi.

Mungkin spesifikasi bagian Pengecekan Tipe dapat disederhanakan dengan mempelajari konsep @keean tentang graf terhubung dari tipe berbeda sebagai simpul untuk algoritme penyatuan. Setiap tipe berbeda yang dihubungkan oleh sebuah edge harus memiliki tipe yang kongruen, dengan edge yang dibuat untuk semua tipe yang terhubung melalui penetapan atau sebaliknya dalam kode sumber. Jika ada serikat dan persimpangan (dari posting saya sebelumnya), maka arah penugasan harus diperhitungkan ( entah bagaimana? ). Setiap jenis yang tidak diketahui yang berbeda dimulai dengan batas atas terkecil (LUB) dari Atas dan batas bawah terbesar (GLB) dari Bawah dan kemudian kendala dapat mengubah batas ini. Jenis yang terhubung harus memiliki batas yang kompatibel. Batasan semua harus berupa batas kelas tipe.

Dalam Implementasi :

Misalnya, selalu mungkin untuk mengimplementasikan fungsi berparameter dengan membuat salinan baru dari fungsi untuk setiap instantiasi, di mana fungsi baru dibuat dengan mengganti parameter tipe dengan argumen tipe.

Saya percaya istilah teknis yang benar adalah monomorfisasi .

Pendekatan ini akan menghasilkan waktu eksekusi yang paling efisien dengan biaya waktu kompilasi ekstra yang cukup besar dan peningkatan ukuran kode. Ini mungkin menjadi pilihan yang baik untuk fungsi parameter yang cukup kecil untuk inline, tetapi itu akan menjadi tradeoff yang buruk dalam kebanyakan kasus lainnya.

Pembuatan profil akan memberi tahu programmer fungsi mana yang paling diuntungkan dari monomorfisasi. Mungkin pengoptimal Java Hotspot melakukan pengoptimalan monomorfisasi saat runtime?

@egonelbre menulis:

Ada Summary of Go Generics Discussions , yang mencoba memberikan gambaran umum tentang diskusi dari berbagai tempat.

Bagian Ikhtisar tampaknya menyiratkan bahwa penggunaan universal referensi tinju Java untuk instance dalam wadah adalah satu-satunya sumbu desain yang secara diametris menentang monomorfisasi template C++. Tetapi batas kelas tipe (yang juga dapat diimplementasikan dengan templat C++ namun selalu monomorfis) diterapkan pada fungsi, bukan pada parameter tipe wadah. Dengan demikian, gambaran umum tidak memiliki sumbu desain untuk kelas tipe di mana kita dapat memilih apakah akan memonomorfisasikan setiap fungsi yang dibatasi kelas tipe. Dengan kelas tipe, kami selalu membuat programmer lebih cepat (lebih sedikit boilerplate) dan bisa mendapatkan keseimbangan yang lebih halus antara membuat kompiler/eksekusi lebih cepat/lebih lambat dan kode mengasapi lebih besar/lebih kecil. Per posting saya sebelumnya, mungkin yang optimal adalah jika pilihan fungsi untuk monomorphise didorong oleh profiler (secara otomatis atau lebih mungkin dengan anotasi).

Di bagian Masalah : Struktur Data Umum :

Kontra

  • Struktur umum cenderung mengakumulasi fitur dari semua penggunaan, menghasilkan peningkatan waktu kompilasi atau kode mengasapi atau membutuhkan tautan yang lebih cerdas.

Untuk kelas tipe ini tidak benar atau kurang menjadi masalah, karena antarmuka hanya perlu diimplementasikan untuk tipe data yang dipasok ke fungsi yang menggunakan antarmuka tersebut. Kelas tipe adalah tentang pengikatan implementasi yang terlambat ke antarmuka, tidak seperti OOP yang mengikat setiap tipe data ke metodenya untuk implementasi class .

Selain itu, tidak semua metode perlu dimasukkan ke dalam satu antarmuka. Klausa requires (bahkan jika secara implisit disimpulkan oleh kompiler) pada kelas tipe yang terikat untuk deklarasi fungsi dapat memadupadankan antarmuka yang diperlukan.

  • Struktur umum dan API yang beroperasi di atasnya cenderung lebih abstrak daripada API yang dibuat khusus, yang dapat membebani kognitif penelepon

Argumen tandingan yang menurut saya secara signifikan memperbaiki kekhawatiran ini adalah bahwa beban kognitif untuk mempelajari sejumlah kasus khusus yang tidak terbatas, implementasi ulang dari algoritma generik yang pada dasarnya sama, tidak terbatas. Padahal, mempelajari API generik abstrak terbatas.

  • Pengoptimalan mendalam sangat non-generik dan spesifik konteks, oleh karena itu lebih sulit untuk mengoptimalkannya dalam algoritme generik.

Ini bukan penipu yang valid. Aturan 80/20 mengatakan jangan menambahkan kompleksitas tak terbatas (misalnya optimasi prematur) untuk kode yang ketika diprofilkan tidak memerlukannya. Pemrogram bebas untuk mengoptimalkan dalam 20% kasus sementara 80% sisanya ditangani oleh kompleksitas terbatas dan beban kognitif API generik.

Apa yang benar-benar kami maksudkan di sini adalah keteraturan bahasa dan bantuan API generik, tidak ada salahnya. Kontra itu benar-benar tidak dikonseptualisasikan dengan benar.

Solusi alternatif:

  • gunakan struktur yang lebih sederhana daripada struktur yang rumit

    • misalnya gunakan map[int]struct{} alih-alih Set

Rob Pike (dan saya juga melihat dia membuat poin itu di video) tampaknya kehilangan poin bahwa wadah generik tidak cukup untuk membuat fungsi generik. Kita membutuhkan T di map[T] sehingga kita dapat meneruskan tipe data generik di sekitar fungsi untuk input, output, dan untuk struct kita sendiri. Generik hanya pada parameter tipe kontainer sepenuhnya tidak cukup untuk mengekspresikan API generik dan API generik diperlukan untuk kompleksitas terbatas dan beban kognitif serta memperoleh keteraturan dalam ekosistem bahasa. Saya juga belum melihat peningkatan level refactoring (sehingga berkurangnya komposisi modul yang tidak dapat dengan mudah di-refactored) yang dibutuhkan kode non-generik, itulah yang menjadi Masalah Ekspresi yang saya sebutkan di posting pertama saya.

Di bagian Pendekatan Umum :

Template paket
Ini adalah pendekatan yang digunakan oleh Modula-3, OCaml, SML (disebut "functors"), dan Ada. Alih-alih menentukan jenis individu untuk spesialisasi, seluruh paket bersifat generik. Anda mengkhususkan paket dengan memperbaiki parameter tipe saat mengimpor.

Saya mungkin salah tetapi ini sepertinya tidak sepenuhnya benar. Fungsi ML (jangan dikelirukan dengan fungsi FP) juga dapat mengembalikan output yang tetap tipe parametrised. Tidak akan ada cara untuk menggunakan algoritme dalam fungsi generik lainnya, jadi modul generik tidak akan dapat digunakan kembali (dengan mengimpor dengan tipe konkret ke) modul generik lainnya. Ini tampaknya merupakan upaya untuk menyederhanakan dan kemudian sepenuhnya kehilangan titik generik, penggunaan kembali modul, dll.

Sebaliknya pemahaman saya adalah bahwa parameter tipe paket (alias modul) memungkinkan kemampuan untuk menerapkan parameter tipe ke pengelompokan struct , interface , dan func .

Sistem tipe yang lebih rumit
Ini adalah pendekatan yang dilakukan Haskell dan Rust.
[…]
Kontra:

  • sulit untuk menyesuaikan dengan bahasa yang lebih sederhana (https://groups.google.com/d/msg/golang-nuts/smT_0BhHfBs/MWwGlB-n40kJ)

Mengutip @ianlancetaylor dalam dokumen tertaut:

Jika Anda percaya itu, maka perlu ditunjukkan bahwa inti dari
peta dan kode irisan di runtime Go tidak umum dalam arti
menggunakan polimorfisme tipe. Ini generik dalam artian terlihat
ketik informasi refleksi untuk melihat cara memindahkan dan membandingkan jenis
nilai-nilai. Jadi kami memiliki bukti dengan keberadaan bahwa menulis dapat diterima
kode "generik" di Go dengan menulis kode non-polimorfik yang menggunakan type
refleksi informasi secara efisien, dan kemudian untuk membungkus kode itu dalam
boilerplate aman tipe waktu kompilasi (dalam kasus peta dan irisan
pelat ketel ini, tentu saja, disediakan oleh kompiler).

Dan itulah yang dilakukan oleh kompiler yang mentranspilasikan dari superset Go dengan tambahan generik, akan ditampilkan sebagai kode Go. Tetapi pembungkusnya tidak akan didasarkan pada beberapa penggambaran seperti paket, karena itu akan kekurangan komposisi yang telah saya sebutkan. Intinya adalah bahwa tidak ada jalan pintas ke sistem tipe generik yang dapat dikomposisi dengan baik. Entah kita melakukannya dengan benar atau tidak melakukan apa pun, karena menambahkan beberapa peretasan yang tidak dapat dikomposisi yang tidak benar-benar generik pada akhirnya akan menciptakan inersia clusterfuck dari tambal sulam setengah-setengah generikitas dan ketidakteraturan kasus sudut dan solusi membuat kode ekosistem Go tidak dapat dimengerti.

Juga benar bahwa kebanyakan orang yang menulis program Go kompleks yang besar memiliki
tidak menemukan kebutuhan yang signifikan untuk obat generik. Sejauh ini lebih seperti
kutil yang menjengkelkan - kebutuhan untuk menulis tiga baris boilerplate untuk
setiap jenis untuk diurutkan--bukan penghalang utama untuk menulis berguna
kode.

Ya, ini telah menjadi salah satu pemikiran di benak saya apakah pergi ke sistem kelas tipe yang meledak sepenuhnya dapat dibenarkan. Jika semua perpustakaan Anda berbasis di sekitarnya, maka tampaknya itu bisa menjadi harmoni yang indah, tetapi jika kita merenungkan tentang kelambanan peretasan Go yang ada untuk generik, maka mungkin sinergi tambahan yang diperoleh akan rendah untuk banyak proyek. ?

Tetapi jika transpiler dari sintaks kelas tipe meniru cara manual yang ada Go dapat memodelkan obat generik (Sunting: yang baru saja saya baca bahwa @andrewcmyers menyatakan masuk akal ), ini mungkin kurang memberatkan dan menemukan sinergi yang bermanfaat. Sebagai contoh, saya menyadari bahwa dua kelas tipe parameter dapat ditiru dengan interface diimplementasikan pada struct yang mengemulasi Tuple, atau @jba menyebutkan ide untuk menggunakan interface inline dalam konteks . Rupanya struct secara struktural alih-alih diketik secara nominal kecuali diberi nama dengan type ? Saya juga mengkonfirmasi metode interface dapat memasukkan interface lain sehingga mungkin dimungkinkan untuk melakukan transpile dari HKT dalam contoh pengurutan Anda yang saya tulis di posting saya sebelumnya di sini. Tapi saya perlu memikirkan ini lebih jauh ketika saya tidak begitu mengantuk.

Saya pikir cukup adil untuk mengatakan bahwa sebagian besar tim Go tidak menyukai C++
templat, di mana satu bahasa lengkap Turing telah dilapisi
bahasa Turing lain yang lengkap sehingga kedua bahasa tersebut memiliki
sintaks yang sama sekali berbeda, dan program dalam kedua bahasa adalah
ditulis dengan cara yang sangat berbeda. Template C++ berfungsi sebagai peringatan
kisah karena implementasi yang kompleks telah merasuki keseluruhan
pustaka standar, menyebabkan pesan kesalahan C++ menjadi sumber
heran dan takjub. Ini bukan jalan yang akan diikuti Go.

Saya ragu siapa pun akan tidak setuju! Manfaat monomorfisasi adalah ortogonal dengan kerugian dari mesin metaprogramming generik lengkap Turing.

Btw, kesalahan desain template C++ bagi saya tampaknya merupakan esensi generatif yang sama dari cacat fungsi ML generatif (sebagai lawan dari aplikatif). Prinsip Kekuatan Terkecil berlaku.


@ianlancetaylor menulis:

Sangat mengecewakan melihat Go menjadi semakin kompleks dengan menambahkan tipe parameter bawaan.

Terlepas dari spekulasi dalam masalah ini, saya pikir ini sangat tidak mungkin terjadi.

Saya berharap begitu. Saya sangat yakin bahwa Go harus menambahkan sistem generik yang koheren atau hanya menerima bahwa ia tidak akan pernah memiliki sistem generik.

Saya pikir garpu ke transpiler lebih mungkin terjadi, sebagian karena saya memiliki dana untuk mengimplementasikannya dan saya tertarik untuk melakukannya. Namun saya masih menganalisis situasinya.

Itu akan merusak ekosistem, tapi setidaknya Go bisa tetap murni dengan prinsip minimalisnya. Jadi untuk menghindari retaknya ekosistem dan memungkinkan beberapa inovasi lain yang saya inginkan, saya mungkin tidak akan menjadikannya superset dan menamakannya Zero sebagai gantinya.

@pciet menulis:

Pilihan saya adalah tidak untuk aplikasi generik yang digeneralisasi, ya untuk lebih banyak fungsi generik bawaan seperti append dan copy yang bekerja pada beberapa tipe dasar. Mungkin sort dan search dapat ditambahkan untuk jenis koleksi?

Memperluas kelembaman ini mungkin akan mencegah fitur generik yang komprehensif untuk membuatnya menjadi Go. Mereka yang menginginkan obat generik cenderung pergi ke padang rumput yang lebih hijau. @andrewcmyers mengulangi ini:

Ini ~adalah~ akan mengecewakan melihat Go menjadi lebih dan lebih kompleks dengan menambahkan tipe parameter bawaan. Akan lebih baik hanya menambahkan dukungan bahasa untuk programmer untuk menulis tipe parameter mereka sendiri.

@shelby3

Afaik, satu-satunya cara untuk melakukan wadah/pengumpulan heterogen di Go sekarang adalah dengan memasukkan semua tipe ke antarmuka kosong{} yang membuang informasi pengetikan dan akan saya anggap memerlukan pemeriksaan tipe cast dan runtime, yang semacam2 mengalahkan titik mengetik statis.

Lihat pola pembungkus dalam komentar di atas untuk pemeriksaan tipe statis kumpulan{} antarmuka di Go.

Intinya adalah bahwa tidak ada jalan pintas ke sistem tipe generik yang dapat dikomposisi dengan baik. Entah kami melakukannya dengan benar atau tidak melakukan apa pun, karena menambahkan beberapa peretasan yang tidak dapat dikomposisi yang sebenarnya bukan obat generik…

Bisakah Anda menjelaskan ini lebih lanjut? Untuk kasus tipe koleksi yang memiliki antarmuka yang mendefinisikan perilaku umum yang diperlukan dari item yang terkandung tampaknya masuk akal untuk menulis fungsi.

@pciet kode ini benar-benar melakukan hal yang persis seperti yang dijelaskan oleh @shelby3 dan mempertimbangkan antipattern. Mengutip Anda dari sebelumnya:

Ini memberikan contoh dokumentasi dan pengujian yang mengetik pola pembungkus (terima kasih @pierrre) untuk keamanan tipe waktu kompilasi dan juga memiliki pemeriksaan refleksi untuk keamanan tipe run-time.

Anda mengambil kode yang tidak memiliki informasi tipe dan, berdasarkan tipe per tipe, menambahkan gips dan inspeksi tipe runtime menggunakan refleksi. Inilah yang dikeluhkan oleh @shelby3 . Saya cenderung menyebut pendekatan ini "monomorphization-by-hand" dan merupakan jenis tugas yang membosankan yang menurut saya paling baik digunakan oleh kompiler.

Pendekatan ini memiliki sejumlah kelemahan:

  • Memerlukan pembungkus tipe demi tipe, dipelihara dengan tangan atau alat seperti go generate
  • (Jika dilakukan dengan tangan alih-alih alat) peluang untuk membuat kesalahan di boilerplate yang tidak akan ditangkap sampai runtime
  • Memerlukan pengiriman dinamis alih-alih pengiriman statis, yang keduanya lebih lambat dan menggunakan lebih banyak memori
  • Menggunakan refleksi runtime daripada pernyataan tipe waktu kompilasi, yang juga lambat
  • Tidak dapat dikomposisi: bertindak sepenuhnya pada tipe konkret tanpa peluang untuk menggunakan batas tipe-kelas (atau bahkan seperti antarmuka) pada tipe, kecuali jika Anda melakukan handroll lapisan tipuan lain untuk setiap antarmuka non-kosong yang juga ingin Anda abstraksi

Bisakah Anda menjelaskan ini lebih lanjut? Untuk kasus tipe koleksi yang memiliki antarmuka yang mendefinisikan perilaku umum yang diperlukan dari item yang terkandung tampaknya masuk akal untuk menulis fungsi.

Sekarang di mana pun Anda ingin menggunakan ikatan alih-alih atau sebagai tambahan untuk tipe beton, Anda juga harus menulis boilerplate pengecekan tipe yang sama untuk setiap tipe antarmuka. Itu hanya semakin menambah ledakan (mungkin kombinatorial) dari pembungkus tipe statis yang harus Anda tulis.

Ada juga ide yang, sejauh yang saya tahu, sama sekali tidak dapat diekspresikan dalam sistem tipe Go saat ini, seperti ikatan pada kombinasi antarmuka. Bayangkan kita memiliki:

type Foo interface {
    ...
}

type Bar interface {
    ...
}

Bagaimana kita mengekspresikan, menggunakan pemeriksaan tipe statis murni, bahwa kita menginginkan tipe yang mengimplementasikan Foo dan Bar ? Sejauh yang saya tahu ini tidak mungkin di Go (singkat dari menggunakan pemeriksaan runtime yang mungkin gagal, menghindari keamanan tipe statis).

Dengan sistem generik berbasis kelas tipe, kita dapat menyatakan ini sebagai:

func baz<T Foo + Bar>(t T) {
    ...
}

@tarcieri

Bagaimana kita mengekspresikan, menggunakan pemeriksaan tipe statis murni, bahwa kita menginginkan tipe yang mengimplementasikan Foo dan Bar?

hanya seperti ini:

type T interface {
    Foo
    Bar
}

func baz(t T) { ... }

@sbinet rapi, TIL

Secara pribadi saya menganggap refleksi runtime sebagai fitur yang salah, tetapi itu hanya saya ... Saya dapat menjelaskan alasannya jika ada yang tertarik.

Saya pikir siapa pun yang mengimplementasikan obat generik dalam bentuk apa pun harus membaca "Elemen Pemrograman" Stepanov beberapa kali terlebih dahulu. Itu akan menghindari banyak masalah Tidak Diciptakan Di Sini dan menemukan kembali roda. Setelah membaca itu harus jelas mengapa "Konsep C++" dan "Haskell Typeclasses" adalah cara yang tepat untuk melakukan obat generik.

Saya melihat masalah ini tampaknya aktif kembali
Ini adalah taman bermain proposal strawman
https://go-li.github.io/test.html
cukup rekatkan program demo dari sini
https://github.com/go-li/demo

Terima kasih banyak atas evaluasi Anda terhadap parameter tunggal ini
fungsi generik.

Kami memelihara gccgo yang diretas dan
proyek ini tidak akan mungkin tanpamu, jadi kami
ingin berkontribusi kembali.

Kami juga menantikan obat generik apa pun yang Anda adopsi, teruslah bekerja dengan baik!

@anlhord di mana detail implementasi tentang ini? Di mana seseorang dapat membaca tentang sintaks? Apa yang diimplementasikan? Apa yang tidak diimplementasikan? Apa spesifikasi untuk implementasi ini? Apa pro dan kontra untuk itu?

Tautan taman bermain berisi kemungkinan terburuk dari ini:

package main

import "fmt"

func main() {
    fmt.Println("Hello, playground")
}

Kode itu tidak memberi tahu saya cara menggunakannya dan apa yang bisa saya uji.

Jika Anda dapat memperbaiki hal-hal itu, itu akan membantu lebih memahami apa proposal Anda dan bagaimana perbandingannya dengan yang sebelumnya / lihat bagaimana poin-poin lain yang diangkat di sini berlaku atau tidak.

Semoga ini membantu Anda memahami masalah dengan komentar Anda.

@joho menulis:

Akankah ada nilai pada literatur akademis untuk panduan dalam mengevaluasi pendekatan?

Satu-satunya makalah yang saya baca tentang topik ini adalah Apakah pengembang mendapat manfaat dari tipe generik ? (maaf paywall, Anda mungkin mencari cara untuk mengunduh pdf di Google) yang memiliki yang berikut untuk dikatakan

Akibatnya, interpretasi konservatif dari eksperimen
adalah bahwa tipe generik dapat dianggap sebagai tradeoff
antara karakteristik dokumentasi positif dan
karakteristik ekstensibilitas negatif.

Saya menganggap OOP dan subkelas (misalnya kelas di Java dan C++) tidak akan dianggap serius karena Go sudah memiliki interface seperti kelas tipe (tanpa parameter tipe generik T eksplisit), Java adalah dikutip sebagai apa yang tidak boleh disalin, dan karena banyak yang berpendapat bahwa itu adalah anti-pola. Upthread Saya telah menautkan ke beberapa argumen itu. Kita bisa masuk lebih dalam ke analisis itu jika ada yang tertarik.

Saya belum mempelajari penelitian yang lebih baru seperti sistem Genus yang disebutkan di atas . Saya waspada terhadap sistem "wastafel dapur" yang mencoba menggabungkan begitu banyak paradigma (misalnya subkelas, pewarisan berganda, OOP, linearisasi sifat, implicit , kelas tipe, tipe abstrak, dll), karena keluhan tentang Scala memiliki begitu banyak kasus sudut dalam praktik, meskipun mungkin itu akan membaik dengan Scala 3 (alias Dotty dan kalkulus DOT). Saya ingin tahu apakah tabel perbandingan mereka membandingkan dengan Scala 3 eksperimental atau versi Scala saat ini?

Jadi biasanya, yang tersisa adalah fungsi ML dan kelas tipe Haskell dalam hal sistem generik yang terbukti, yang secara signifikan meningkatkan ekstensibilitas dan fleksibilitas dibandingkan dengan subkelas OOP+.

Saya menuliskan beberapa diskusi pribadi @keean dan saya tentang modul functor ML versus kelas tipe. Sorotan tampaknya:

  • typeclasses _modelkan aljabar_ (tetapi tanpa aksioma yang dicentang ) dan implementasikan setiap tipe data untuk setiap antarmuka hanya dengan satu cara. Sehingga memungkinkan pemilihan implisit dari implementasi oleh kompiler tanpa anotasi di situs panggilan.

  • Fungsi aplikatif memiliki transparansi referensial sedangkan fungsi generatif membuat instance baru pada setiap instantiasi, yang berarti mereka bukan invarian urutan inisialisasi.

  • Fungsi ML lebih kuat/fleksibel daripada kelas tipe, tetapi hal ini memerlukan lebih banyak anotasi dan berpotensi lebih banyak interaksi kasus sudut. Dan menurut @keean mereka memerlukan tipe dependen (untuk tipe terkait ) yang merupakan sistem tipe yang lebih kompleks. @keean berpikir _ekspresi generikitas Stepanov sebagai aljabar_ plus kelas tipe cukup kuat dan fleksibel , sehingga tampaknya menjadi sweet spot untuk generik yang canggih, terbukti dengan baik (di Haskell dan sekarang di Rust). Namun, aksioma tidak ditegakkan oleh kelas tipe.

  • Saya telah menyarankan untuk menambahkan serikat pekerja untuk wadah heterogen dengan kelas tipe untuk diperluas di sepanjang sumbu lain dari Masalah Ekspresi, meskipun ini membutuhkan kekekalan atau penyalinan (hanya untuk kasus di mana ekstensibilitas heterogen digunakan) yang diketahui memiliki O(log n) perlambatan dibandingkan dengan imperativity bisa berubah tak terkendali.

@larsth menulis:

Mungkin menarik untuk memiliki satu atau lebih transpiler eksperimental - kode sumber generik Go ke kompiler kode sumber Go 1.xy.

PS Saya ragu Go akan mengadopsi sistem pengetikan yang begitu canggih, tetapi saya sedang mempertimbangkan transpiler ke sintaks Go yang ada seperti yang saya sebutkan di posting saya sebelumnya (lihat edit di bagian bawah). Dan saya ingin sistem generik yang kuat bersama dengan fitur Go yang sangat diinginkan itu. Obat generik kelas tipe di Go tampaknya seperti yang saya inginkan.

@bcmils menulis tentang proposalnya tentang fungsi waktu kompilasi untuk generik:

Saya pernah mendengar argumen yang sama yang digunakan untuk menganjurkan mengekspor tipe interface daripada tipe konkret di Go API, dan kebalikannya ternyata lebih umum: abstraksi prematur membatasi tipe dan menghalangi ekstensi API. (Untuk satu contoh seperti itu, lihat #19584.) Jika Anda ingin mengandalkan garis argumen ini, saya pikir Anda perlu memberikan beberapa contoh konkret.

Memang benar bahwa abstraksi sistem tipe harus mengabaikan beberapa derajat kebebasan dan kadang-kadang kita telah keluar dari batasan tersebut dengan "tidak aman" (yaitu melanggar abstraksi yang diperiksa secara statis), namun itu harus ditukar dengan manfaat dari decoupling modular dengan invarian beranotasi ringkas.

Saat merancang sistem untuk generik, kami cenderung ingin meningkatkan keteraturan dan prediktabilitas ekosistem sebagai salah satu tujuan utama, terutama jika filosofi inti Go dipertimbangkan (misalnya rata-rata pemrogram adalah prioritas).

Prinsip Kekuatan Terkecil berlaku. Kekuatan/fleksibilitas fungsi waktu kompilasi "tersembunyi" invarian untuk generikitas harus ditimbang terhadap kemampuannya untuk membahayakan misalnya keterbacaan kode sumber dalam ekosistem (di mana decoupling modular sangat penting karena pembaca tidak 'tidak perlu membaca jumlah kode yang berpotensi tidak terbatas karena dependensi transitif implisit, untuk memahami modul/paket yang diberikan!). Resolusi implisit contoh implementasi kelas tipe memiliki masalah ini jika aljabar mereka tidak dipatuhi .

Tentu, tapi itu sudah berlaku untuk banyak kendala implisit di Go yang terlepas dari mekanisme pemrograman umum apa pun.

Misalnya, suatu fungsi dapat menerima parameter tipe antarmuka dan awalnya memanggil metodenya secara berurutan. Jika fungsi itu kemudian berubah untuk memanggil metode tersebut secara bersamaan (dengan memunculkan goroutine tambahan), batasan "harus aman untuk penggunaan bersamaan" tidak tercermin dalam sistem tipe.

Namun afaik Go tidak mencoba merancang abstraksi untuk memodulasi efek tersebut. Rust memiliki abstraksi seperti itu (yang menurut saya berlebihan pita/tsuris/membatasi untuk beberapa/sebagian besar kasus penggunaan dan saya berpendapat untuk abstraksi model utas tunggal yang lebih mudah namun sayangnya Go tidak mendukung pembatasan semua goroutine yang muncul ke utas yang sama ) . Dan Haskell memerlukan kontrol monadik atas efek karena menegakkan fungsi murni untuk transparansi referensial .


@alercah menulis:

Saya pikir kelemahan terbesar dari batasan yang disimpulkan adalah mereka membuatnya mudah untuk menggunakan tipe dengan cara yang memperkenalkan batasan tanpa sepenuhnya memahaminya. Dalam kasus terbaik, ini hanya berarti bahwa pengguna Anda mungkin mengalami kegagalan waktu kompilasi yang tidak terduga, tetapi dalam kasus terburuk, ini berarti Anda dapat merusak paket untuk konsumen dengan memperkenalkan batasan baru secara tidak sengaja. Batasan yang ditentukan secara eksplisit akan menghindari hal ini.

Sepakat. Mampu secara diam-diam memecahkan kode di modul lain karena invarian dari tipe tidak dijelaskan secara eksplisit sangat berbahaya.


@andrewcmyers menulis:

Untuk lebih jelasnya, saya tidak menganggap pembuatan kode gaya makro, baik yang dilakukan dengan gen, cpp, gofmt -r, atau alat makro/templat lainnya, sebagai solusi yang baik untuk masalah generik bahkan jika distandarisasi. Ini memiliki masalah yang sama dengan template C++: kode mengasapi, kurangnya pemeriksaan tipe modular, dan kesulitan debugging. Ini menjadi lebih buruk saat Anda mulai, seperti biasa, membangun kode generik dalam hal kode generik lainnya. Menurut saya, keuntungannya terbatas: itu akan membuat hidup relatif sederhana untuk penulis kompiler Go dan menghasilkan kode yang efisien — kecuali ada tekanan cache instruksi, situasi yang sering terjadi dalam perangkat lunak modern!

@keean tampaknya setuju dengan Anda.

@shelby3 terima kasih atas komentarnya. Bisakah Anda lain kali membuat komentar / pengeditan langsung di dokumen itu sendiri. Lebih mudah untuk melacak di mana hal-hal perlu diperbaiki dan lebih mudah untuk memastikan bahwa semua catatan mendapatkan respons yang tepat.

Bagian Ikhtisar tampaknya menyiratkan bahwa penggunaan universal referensi tinju Java untuk instance ...

Menambahkan komentar untuk memperjelas, bahwa itu tidak dimaksudkan sebagai daftar lengkap. Ini terutama ada sehingga orang mendapatkan inti dari pertukaran yang berbeda. Daftar lengkap pendekatan yang berbeda lebih lanjut di bawah ini.

Struktur umum cenderung mengakumulasi fitur dari semua penggunaan, menghasilkan peningkatan waktu kompilasi atau kode mengasapi atau membutuhkan tautan yang lebih cerdas.
Untuk kelas tipe ini tidak benar atau kurang menjadi masalah, karena antarmuka hanya perlu diimplementasikan untuk tipe data yang dipasok ke fungsi yang menggunakan antarmuka tersebut. Kelas tipe adalah tentang pengikatan implementasi yang terlambat ke antarmuka, tidak seperti OOP yang mengikat setiap tipe data ke metodenya untuk implementasi kelas.

Pernyataan itu adalah tentang apa yang terjadi pada struktur data generik dalam jangka panjang. Dengan kata lain, struktur data generik sering berakhir dengan mengumpulkan semua kegunaan yang berbeda -- daripada memiliki beberapa implementasi yang lebih kecil untuk tujuan yang berbeda. Sebagai contoh lihat https://www.scala-lang.org/api/2.12.3/scala/collection/immutable/List.html.

Penting untuk dicatat bahwa, hanya "desain mekanis" dan "fleksibilitas sebanyak mungkin" tidak cukup untuk menciptakan "solusi generik" yang baik. Itu juga membutuhkan instruksi yang baik, bagaimana sesuatu harus digunakan dan apa yang harus dihindari, dan mempertimbangkan bagaimana orang akhirnya menggunakannya.

Struktur umum dan API yang beroperasi di atasnya cenderung lebih abstrak daripada API yang dibuat khusus ...

Argumen tandingan yang menurut saya secara signifikan memperbaiki kekhawatiran ini adalah bahwa beban kognitif untuk mempelajari sejumlah kasus khusus yang tidak terbatas, implementasi ulang dari algoritma generik yang pada dasarnya sama, tidak terbatas ...

Menambahkan catatan tentang beban kognitif dari banyak API serupa.

Implementasi ulang kasus khusus tidak terbatas dalam praktiknya. Anda hanya akan melihat sejumlah spesialisasi.

Ini bukan penipu yang valid.

Anda mungkin tidak setuju dengan beberapa poin, saya tidak setuju dengan beberapa poin, tetapi saya memahami sudut pandang mereka dan mencoba memahami masalah yang dihadapi orang sehari-hari. Tujuan dari dokumen tersebut adalah untuk mengumpulkan pendapat yang berbeda, bukan untuk menilai "betapa menyebalkannya sesuatu bagi seseorang".

Namun, dokumen tersebut mengambil sikap pada "masalah yang dapat dilacak ke masalah dunia nyata", karena masalah yang abstrak dan difasilitasi di forum cenderung menjadi obrolan yang tidak berarti tanpa pemahaman apa pun yang dibangun.

Apa yang benar-benar kami maksudkan di sini adalah keteraturan bahasa dan bantuan API generik, tidak ada salahnya.

Tentu dalam praktiknya Anda mungkin memerlukan gaya pengoptimalan ini hanya untuk kurang dari 1% kasus.

Solusi alternatif:

Solusi alternatif tidak dimaksudkan sebagai pengganti obat generik. Tapi, lebih kepada daftar solusi potensial untuk berbagai jenis masalah.

Template paket

Saya mungkin salah tetapi ini sepertinya tidak sepenuhnya benar. Fungsi ML (jangan dikelirukan dengan fungsi FP) juga dapat mengembalikan output yang tetap tipe parametrised.

Bisakah Anda memberikan kata-kata yang lebih jelas dan jika perlu dibagi menjadi dua pendekatan yang berbeda?

@egonelbre terima kasih juga telah menanggapi sehingga saya dapat mengetahui poin mana yang perlu saya klarifikasi lebih lanjut.

Bisakah Anda lain kali membuat komentar / pengeditan langsung di dokumen itu sendiri.

Mohon maaf jika saya dapat mematuhinya tetapi saya tidak pernah menggunakan fitur diskusi Google Doc, tidak punya waktu untuk mempelajarinya, dan saya juga lebih suka dapat menautkan ke diskusi saya di Github untuk referensi di masa mendatang.

Sebagai contoh lihat https://www.scala-lang.org/api/2.12.3/scala/collection/immutable/List.html.

Desain perpustakaan koleksi Scala dikritik oleh banyak orang, termasuk salah satu mantan anggota tim kunci mereka . Sebuah komentar yang diposting ke LtU mewakili. Catatan Saya menambahkan yang berikut ini ke salah satu posting saya sebelumnya di utas ini untuk mengatasi ini:

Saya waspada terhadap sistem "wastafel dapur" yang mencoba mencampur begitu banyak paradigma (misalnya subkelas, pewarisan berganda, OOP, linearisasi sifat, implicit , kelas tipe, tipe abstrak, dll), karena keluhan tentang Scala memiliki begitu banyak kasus sudut dalam praktik, meskipun mungkin itu akan membaik dengan Scala 3 (alias Dotty dan kalkulus DOT).

Saya tidak berpikir perpustakaan koleksi Scala akan mewakili perpustakaan yang dibuat untuk PL dengan hanya kelas tipe untuk polimorfisme. Selain itu, koleksi Scala menggunakan anti-pola pewarisan , yang menyebabkan hierarki kompleks, dikombinasikan dengan implicit helper seperti CanBuildFrom yang meledakkan anggaran kompleksitas. Dan saya pikir jika poin @keean dipatuhi tentang _Elements of Programming_ Stepanov sebagai aljabar , perpustakaan koleksi yang elegan dapat dibuat. Itu adalah alternatif pertama yang saya lihat untuk perpustakaan koleksi berbasis functor (FP) (yaitu tidak menyalin Haskell ) juga berdasarkan matematika. Saya ingin melihat ini dalam praktik, yang merupakan salah satu alasan saya berkolaborasi/berdiskusi dengannya tentang desain PL baru. Dan mulai saat ini, saya berencana untuk mengubah bahasa itu menjadi Go (walaupun saya sudah bertahun-tahun mencoba mencari cara untuk menghindarinya). Jadi mudah-mudahan kita bisa segera bereksperimen untuk melihat bagaimana hasilnya.

Persepsi saya adalah bahwa komunitas/filsafat Go lebih suka menunggu untuk melihat apa yang berhasil dalam praktik dan mengadopsinya nanti setelah terbukti, daripada terburu-buru dan mencemari bahasa dengan eksperimen yang gagal. Karena seperti yang Anda ulangi, semua klaim abstrak ini tidak begitu konstruktif (kecuali mungkin untuk ahli teori desain PL). Juga mungkin tidak masuk akal untuk merancang sistem generik yang koheren oleh komite.

Itu juga membutuhkan instruksi yang baik, bagaimana sesuatu harus digunakan dan apa yang harus dihindari, dan mempertimbangkan bagaimana orang akhirnya menggunakannya.

Dan saya pikir itu akan membantu untuk tidak mencampur begitu banyak paradigma berbeda yang tersedia untuk programmer dalam bahasa yang sama. Tampaknya tidak perlu ( @keean dan saya perlu membuktikan klaim itu). Saya pikir kami berdua menganggap filosofi bahwa anggaran kompleksitas terbatas dan apa yang Anda tinggalkan dari PL yang sama pentingnya dengan fitur yang disertakan.

Namun, dokumen tersebut mengambil sikap pada "masalah yang dapat dilacak ke masalah dunia nyata", karena masalah yang abstrak dan difasilitasi di forum cenderung menjadi obrolan yang tidak berarti tanpa pemahaman apa pun yang dibangun.

Sepakat. Dan juga sulit bagi semua orang untuk mengikuti poin-poin abstrak. Iblis ada di detail dan hasil aktual di alam liar.

Tentu dalam praktiknya Anda mungkin memerlukan gaya pengoptimalan ini hanya untuk kurang dari 1% kasus.

Go sudah memiliki interface untuk generik, sehingga dapat menangani kasus di mana tidak perlu polimorfisme parametrik pada tipe T untuk instance antarmuka yang disediakan oleh situs panggilan.

Saya pikir membaca di suatu tempat, mungkin itu upthread, argumen bahwa sebenarnya perpustakaan standar Go menderita inkonsistensi penggunaan optimal dari idiom terbaru. Saya tidak tahu apakah itu benar, karena saya belum berpengalaman dengan Go. Maksud saya adalah bahwa paradigma generik yang dipilih menginfeksi semua perpustakaan. Jadi ya sampai sekarang Anda dapat mengklaim hanya 1% dari kode yang membutuhkannya, karena sudah ada inersia dalam idiom yang menghindari kebutuhan akan obat generik.

Anda mungkin benar. Saya juga skeptis tentang seberapa banyak saya akan menggunakan fitur bahasa tertentu. Saya pikir eksperimen untuk mencari tahu adalah cara saya akan melanjutkan. Perancangan PL merupakan proses yang iteratif, maka permasalahan yang melawan inersia yang berkembang membuat proses iterasi menjadi sulit. Jadi saya kira Rob Pike benar dalam video di mana dia menyarankan menulis program yang menulis kode untuk program (artinya pergi menulis alat generasi dan transpiler) untuk bereksperimen dan menguji ide.

Ketika kami dapat menunjukkan bahwa serangkaian fitur tertentu lebih unggul dalam praktik (dan mudah-mudahan juga popularitas penggunaan) dibandingkan yang saat ini ada di Go, maka kami mungkin dapat melihat beberapa bentuk konsensus seputar menambahkannya ke Go. Saya mendorong orang lain untuk juga membuat sistem eksperimental yang berubah menjadi Go.

Bisakah Anda memberikan kata-kata yang lebih jelas dan jika perlu dibagi menjadi dua pendekatan yang berbeda?

Saya menambahkan suara saya kepada mereka yang ingin mencegah upaya untuk menempatkan beberapa fitur templating yang terlalu sederhana di Go dan mengklaim itu generik. IOW, saya pikir sistem generik yang berfungsi dengan baik yang tidak akan berakhir dengan inersia buruk pada dasarnya tidak sesuai dengan keinginan untuk memiliki desain yang terlalu sederhana untuk obat generik. Afaik, sistem generik membutuhkan desain holistik yang dipikirkan dengan matang dan terbukti dengan baik. Menggemakan apa yang @larsth tulis , saya mendorong mereka yang memiliki proposal serius untuk terlebih dahulu membuat transpiler (atau mengimplementasikannya dalam fork dari gccgo frontend) dan kemudian bereksperimen dengan proposal tersebut sehingga kita semua dapat lebih memahami batasannya. Saya didorong untuk membaca upthread bahwa @ianlancetaylor tidak berpikir polusi inersia buruk akan ditambahkan ke Go. Adapun keluhan khusus saya tentang proposal parametrisasi tingkat paket, saran saya untuk siapa yang pernah mengusulkannya, silakan renungkan apakah akan membuat kompiler yang bisa kita semua gunakan untuk bermain dan kemudian kita semua bisa berbicara tentang contoh apa yang kita suka dan tidak' tidak suka tentang itu. Kalau tidak, kita berbicara melewati satu sama lain karena mungkin saya bahkan tidak mengerti dengan benar proposal seperti yang dijelaskan secara abstrak. Saya pasti tidak mengerti proposalnya, karena saya tidak mengerti bagaimana paket parametrised dapat digunakan kembali di paket lain yang juga parametrised. TKI, jika sebuah paket membutuhkan parameter, maka paket tersebut juga perlu membuat instance paket lain dengan parameter. Tetapi tampaknya proposal tersebut menyatakan bahwa satu-satunya cara untuk membuat instance paket parametrised adalah dengan tipe beton, bukan parameter tipe.

Permintaan maaf yang begitu bertele-tele. Ingin memastikan saya tidak salah paham.

@shelby3 ah, saya kemudian salah memahami keluhan awal. Pertama saya harus menjelaskan bahwa bagian dalam "Pendekatan Generik" bukanlah proposal yang konkret. Mereka adalah pendekatan atau, dengan kata lain, keputusan desain yang lebih besar yang mungkin diambil seseorang dalam pendekatan generik yang konkret. Namun, pengelompokan tersebut sangat dimotivasi oleh implementasi yang ada atau proposal konkrit/informal. Juga, saya menduga setidaknya ada 5 ide besar yang masih hilang dari daftar itu.

Untuk pendekatan "template paket" ada dua variasi (lihat diskusi terkait dalam dokumen):

  1. paket generik berbasis "antarmuka",
  2. paket generik secara eksplisit.

Untuk 1. tidak memerlukan paket generik untuk melakukan sesuatu yang istimewa -- misalnya container/ring saat ini akan dapat digunakan untuk spesialisasi. Bayangkan "spesialisasi" di sini sebagai mengganti semua instance antarmuka dalam paket dengan tipe konkret (dan mengabaikan impor melingkar). Ketika paket itu sendiri mengkhususkan paket lain, ia dapat menggunakan "antarmuka" sebagai spesialisasi - maka penggunaan ini juga akan terspesialisasi.

Untuk 2. Anda dapat melihatnya dalam dua cara. Salah satunya adalah spesialisasi beton rekursif pada setiap impor -- mirip dengan templating/makro, tidak akan ada "paket yang diterapkan sebagian". Tentu saja dapat juga dilihat dari sisi fungsional, bahwa paket generik adalah parsial dengan parameter dan kemudian Anda mengkhususkannya.

Jadi, ya, Anda dapat menggunakan satu paket berparameter di paket lain.

Menggemakan apa yang @larsth tulis, saya mendorong mereka yang memiliki proposal serius untuk terlebih dahulu membuat transpiler (atau mengimplementasikannya di fork frontend gccgo) dan kemudian bereksperimen dengan proposal tersebut sehingga kita semua dapat lebih memahami batasannya.

Saya tahu ini tidak secara eksplisit diarahkan pada pendekatan itu, tetapi ia memiliki 4 prototipe berbeda untuk menguji gagasan tersebut. Tentu saja, mereka bukan transpiler penuh, tetapi mereka cukup untuk menguji beberapa ide. yaitu saya tidak yakin apakah ada yang telah menerapkan kasus "menggunakan paket berparameter dari yang lain".

Paket berparameter terdengar sangat mirip dengan modul ML (dan fungsi ML adalah parameternya bisa berupa paket lain). Ada dua cara ini dapat bekerja "aplikatif" atau "generatif". Fungsi aplikatif seperti nilai, atau tipe-alias. Fungsi generatif harus dibangun dan setiap instance berbeda. Cara lain untuk memikirkan hal ini, adalah agar sebuah paket bersifat aplikatif, paket itu harus murni (yaitu tidak ada variabel yang bisa berubah di tingkat paket). Jika ada status di tingkat paket, itu harus generatif karena status itu perlu diinisialisasi, dan penting "instance" dari paket generatif mana yang benar-benar Anda lewati sebagai parameter ke paket lain yang pada gilirannya harus generatif. Misalnya paket Ada bersifat generatif.

Masalah dengan pendekatan paket generatif adalah ia menciptakan banyak boilerplate, di mana Anda membuat instance paket dengan parameter. Anda dapat melihat generik Ada untuk melihat seperti apa tampilannya.

Kelas tipe menghindari boilerplate ini dengan memilih kelas tipe secara implisit berdasarkan tipe yang digunakan dalam fungsi saja. Anda juga dapat melihat kelas tipe sebagai kelebihan muatan terbatas dengan pengiriman ganda, di mana resolusi kelebihan muatan hampir selalu terjadi secara statis pada waktu kompilasi, dengan pengecualian untuk rekursi polimorfik dan tipe eksistensial (yang pada dasarnya adalah varian yang tidak dapat Anda singkirkan, Anda hanya dapat menggunakan antarmuka yang dikonfirmasi oleh varian).

Fungsi aplikatif seperti nilai, atau tipe-alias. Fungsi generatif harus dibangun dan setiap instance berbeda. Cara lain untuk memikirkan hal ini, adalah agar sebuah paket bersifat aplikatif, paket itu harus murni (yaitu tidak ada variabel yang bisa berubah di tingkat paket). Jika ada status di tingkat paket, itu harus generatif karena status itu perlu diinisialisasi, dan penting "instance" dari paket generatif mana yang benar-benar Anda lewati sebagai parameter ke paket lain yang pada gilirannya harus generatif. Misalnya paket Ada bersifat generatif.

Terima kasih atas terminologi yang tepat, saya perlu berpikir bagaimana mengintegrasikan ide-ide ini ke dalam dokumen.

Juga, saya tidak dapat melihat alasan mengapa Anda tidak dapat memiliki "tipe-alias otomatis untuk paket yang dihasilkan" - dalam arti sesuatu antara pendekatan "functor aplikatif" dan "fungtor generatif". Jelas, ketika paket tersebut memang berisi beberapa bentuk status, itu bisa menjadi rumit untuk di-debug dan dipahami.

Masalah dengan pendekatan paket generatif adalah ia menciptakan banyak boilerplate, di mana Anda membuat instance paket dengan parameter. Anda dapat melihat generik Ada untuk melihat seperti apa tampilannya.

Sejauh yang saya lihat, itu akan membuat lebih sedikit boilerplate daripada template C++ tetapi lebih dari kelas tipe. Apakah Anda memiliki program dunia nyata yang bagus untuk Ada yang menunjukkan masalahnya? _(Dengan dunia nyata, maksud saya kode yang digunakan/digunakan seseorang dalam produksi.)_

Tentu, lihat go-board Ada saya: https://github.com/keean/Go-Board-Ada/blob/master/go.adb

Meskipun ini adalah definisi produksi yang cukup longgar, kodenya dioptimalkan, berkinerja sebaik versi C++, dan sumber terbukanya, dan algoritmenya telah disempurnakan selama beberapa tahun. Anda juga dapat melihat versi C++: https://github.com/keean/Go-Board/blob/master/go.cpp

Ini menunjukkan (saya pikir) bahwa Ada generik adalah solusi yang lebih rapi daripada template C++ (tapi itu tidak sulit), di sisi lain sulit untuk melakukan akses cepat ke struktur data di Ada karena pembatasan mengembalikan referensi .

Jika Anda ingin melihat sistem generik paket untuk bahasa imperatif, saya pikir Ada adalah salah satu yang terbaik untuk dilihat. Sayang sekali mereka memutuskan untuk menjadi multi-paradigma dan menambahkan semua hal OO ke Ada. Ada adalah Pascal yang disempurnakan, dan Pascal adalah bahasa yang kecil dan elegan. Pascal plus Ada generik masih merupakan bahasa yang cukup kecil, tetapi akan jauh lebih baik menurut saya. Karena fokus Ada bergeser ke pendekatan OO menemukan dokumentasi yang baik dan contoh bagaimana melakukan hal yang sama dengan obat generik tampaknya sulit ditemukan.

Meskipun saya pikir kelas tipe memiliki beberapa keuntungan, saya bisa hidup dengan generik gaya Ada, ada beberapa masalah yang menahan saya untuk menggunakan Ada secara lebih luas, saya pikir itu salah nilai/objek (saya pikir sangat sedikit bahasa yang benar, 'C' menjadi satu-satunya), sulit untuk bekerja dengan pointer (variabel akses) dan untuk membuat abstraksi safe-pointer, dan itu tidak menyediakan cara untuk menggunakan paket dengan polimorfisme runtime (ini menyediakan model objek untuk ini, tetapi ia menambahkan paradigma yang sama sekali baru alih-alih mencoba menemukan cara untuk memiliki polimorfisme runtime menggunakan paket).

Solusi untuk polimorfisme runtime adalah membuat paket kelas satu sehingga instance tanda tangan paket dapat diteruskan sebagai argumen fungsi, sayangnya ini memerlukan tipe dependen (lihat pekerjaan yang dilakukan pada Tipe Objek Dependen untuk Scala untuk membersihkan kekacauan yang mereka buat tipe-sistem aslinya).

Jadi saya pikir obat generik paket dapat bekerja, tetapi Ada beberapa dekade untuk menangani semua kasus tepi, jadi saya akan melihat sistem generik produksi untuk melihat penyempurnaan apa yang digunakan dalam produksi yang dihasilkan. Namun Ada masih gagal karena paketnya bukan kelas satu, dan tidak dapat digunakan dalam polimorfisme runtime, dan ini perlu ditangani.

@keean menulis :

Secara pribadi saya menganggap refleksi runtime sebagai fitur yang salah, tetapi itu hanya saya ... Saya dapat menjelaskan alasannya jika ada yang tertarik.

Penghapusan tipe memungkinkan "Teorema gratis", yang memiliki implikasi praktis . Dapat ditulis (dan mungkin bahkan dapat dibaca karena hubungan transitif dengan kode imperatif?) refleksi runtime membuat tidak mungkin untuk menjamin transparansi referensial dalam kode apa pun dan dengan demikian pengoptimalan kompiler tertentu tidak dimungkinkan dan mengetik monad yang aman tidak dimungkinkan. Saya menyadari Rust bahkan belum memiliki fitur kekekalan. OTOH, refleksi memungkinkan pengoptimalan lain yang tidak mungkin dilakukan jika tidak dapat diketik secara statis.

Saya juga telah menyatakan upthread:

Dan itulah yang dilakukan oleh kompiler yang mentranspilasikan dari superset Go dengan tambahan generik, akan ditampilkan sebagai kode Go. Tetapi pembungkusnya tidak akan didasarkan pada beberapa penggambaran seperti paket, karena itu akan kekurangan komposisi yang telah saya sebutkan. Intinya adalah bahwa tidak ada jalan pintas ke sistem tipe generik yang dapat dikomposisi dengan baik. Entah kita melakukannya dengan benar atau tidak melakukan apa pun, karena menambahkan beberapa peretasan yang tidak dapat dikomposisi yang tidak benar-benar generik pada akhirnya akan menciptakan inersia clusterfuck dari tambal sulam setengah-setengah generikitas dan ketidakteraturan kasus sudut dan solusi membuat kode ekosistem Go tidak dapat dimengerti.


@keean menulis:

[…] agar sebuah paket menjadi aplikatif, paket itu harus murni (tidak ada variabel yang bisa berubah di tingkat paket)

Dan tidak ada fungsi tidak murni yang dapat digunakan untuk menginisialisasi variabel yang tidak dapat diubah.

@egonelbre menulis:

Jadi, ya, Anda dapat menggunakan satu paket berparameter di paket lain.

Apa yang tampaknya saya pikirkan adalah "paket parametrised kelas satu" dan polimorfisme runtime (alias dinamis) yang sepadan yang kemudian disebutkan oleh @keean , karena saya menganggap paket parametrised diusulkan sebagai pengganti kelas tipe atau OOP.

EDIT: tetapi ada dua kemungkinan arti untuk modul "kelas satu": modul sebagai nilai kelas satu seperti di Successor ML dan MixML yang dibedakan dari modul sebagai nilai kelas satu dengan tipe kelas satu seperti dalam 1ML, dan tradeoff yang diperlukan dalam rekursi modul (yaitu pencampuran ) di antara mereka.

@keean menulis:

Solusi untuk polimorfisme runtime adalah membuat paket kelas satu sehingga instance tanda tangan paket dapat diteruskan sebagai argumen fungsi, sayangnya ini memerlukan tipe dependen (lihat pekerjaan yang dilakukan pada Tipe Objek Dependen untuk Scala untuk membersihkan kekacauan yang mereka buat tipe-sistem aslinya).

Apakah yang Anda maksud: tipe dependen (EDIT: Saya kira sekarang yang dia maksud adalah pengetikan "tidak bergantung pada nilai", yaitu " fungsi yang tipe hasilnya bergantung pada argumen [runtime?] [tipe]") Tentu saja tidak tergantung pada nilai misalnya int data, seperti di Idris. Saya pikir Anda mengacu pada pengetikan yang bergantung (yaitu pelacakan) jenis nilai yang mewakili instance modul yang dipakai di hierarki panggilan sehingga fungsi polimorfik seperti itu dapat dimonomorfisasi pada waktu kompilasi? Apakah polimorfisme runtime masuk karena tipe monomorfis seperti itu menjadi tipe eksistensial yang terikat untuk tipe dinamis? Modul F-ing menunjukkan bahwa tipe "tergantung" tidak mutlak diperlukan untuk memodelkan modul ML di sistem F . Sudahkah saya menyederhanakan jika saya menganggap @rossberg memformulasi ulang model pengetikan untuk menghapus semua persyaratan monomorfisasi?

Masalah dengan pendekatan paket generatif adalah bahwa ia menciptakan banyak boilerplate [...]
Kelas tipe menghindari boilerplate ini dengan memilih kelas tipe secara implisit berdasarkan tipe yang digunakan dalam fungsi saja.

Bukankah ada juga boilerplate dengan fungsi ML aplikatif? Tidak ada penyatuan kelas tipe dan fungsi (modul) ML yang diketahui yang mempertahankan singkatnya tanpa memperkenalkan batasan yang diperlukan untuk mencegah (cf juga ) anti-modularitas yang melekat pada kriteria keunikan global dari instance implementasi kelas tipe.

Kelas tipe hanya dapat mengimplementasikan setiap tipe dalam satu cara dan sebaliknya memerlukan newtype wrapper boilerplate untuk mengatasi batasan. Berikut adalah contoh lain dari berbagai cara untuk mengimplementasikan suatu algoritme. Afaics, @keean mengatasi batasan ini dalam contoh pengurutan kelas tipenya dengan mengganti pilihan implisit dengan Relation yang dipilih secara eksplisit menggunakan tipe pembungkus data untuk memberi nama hubungan yang berbeda secara umum pada tipe nilai, tetapi saya ragu apakah taktik tersebut umum untuk semua varian modularitas. Namun solusi yang lebih umum (yang dapat membantu dalam memperbaiki masalah modularitas keunikan global yang mungkin dikombinasikan dengan pembatasan anak yatim sebagai peningkatan pada versi yang diusulkan untuk resolusi anak yatim dengan menggunakan non-default untuk implementasi yang dapat menjadi yatim piatu) mungkin memiliki parameter tipe tambahan secara implisit pada semua kelas tipe interface , yang ketika tidak ditentukan default ke pencocokan implisit normal, tetapi ketika ditentukan (atau ketika tidak ditentukan tidak cocok dengan 2 lainnya) kemudian memilih implementasi yang memiliki nilai yang sama dalam daftar nilai kustom yang dipisahkan koma (jadi ini lebih umum, pencocokan modular daripada menamai instance implement tertentu). Daftar yang dipisahkan koma adalah agar implementasi dapat dibedakan dalam lebih dari satu derajat kebebasan, seperti jika memiliki dua spesialisasi ortogonal. Khusus non-default yang diinginkan dapat ditentukan baik di deklarasi fungsi atau situs panggilan. Di situs panggilan, misalnya f<non-default>(…) .

Jadi mengapa kita membutuhkan modul parametris jika kita memiliki kelas tipe? Afaics hanya untuk (← tautan penting untuk diklik) substitusi karena menggunakan kembali kelas tipe untuk tujuan itu tidak cocok dengan itu misalnya kami ingin modul paket dapat menjangkau banyak file dan kami ingin dapat secara implisit membuka konten modul ke dalam ruang lingkup tanpa boilerplate tambahan . Jadi mungkin maju dengan parameter paket _sintaksis-only_ substitusi-saja (bukan kelas satu) adalah langkah pertama yang masuk akal yang dapat mengatasi generikitas tingkat modul sambil tetap terbuka untuk kompatibilitas dengan dan fungsionalitas yang tidak tumpang tindih jika menambahkan kelas tipe nanti untuk tingkat fungsi kedermawanan. makro ini misalnya diketik atau hanya substitusi sintaksis (alias "praprosesor"). Jika diketik, maka modul menduplikasi fungsionalitas kelas tipe, yang tidak diinginkan baik dari sudut pandang meminimalkan paradigma/konsep yang tumpang tindih dari PL dan kasus sudut potensial karena interaksi tumpang tindih (seperti ketika mencoba menawarkan fungsi dan kelas tipe ML ). Modul yang diketik lebih modular karena modifikasi pada implementasi yang dienkapsulasi di dalam modul yang tidak mengubah tanda tangan yang diekspor tidak dapat menyebabkan konsumen modul menjadi tidak kompatibel (selain masalah anti-modularitas yang disebutkan di atas dari contoh implementasi tumpang tindih kelas tipe). Saya tertarik untuk membaca pemikiran @keean tentang ini.

[…] dengan pengecualian untuk rekursi polimorfik dan tipe eksistensial (yang pada dasarnya adalah varian yang tidak dapat Anda singkirkan, Anda hanya dapat menggunakan antarmuka yang dikonfirmasi oleh varian).

Untuk membantu pembaca lainnya. Dengan "rekursi polimorfik", saya pikir mengacu pada tipe berperingkat lebih tinggi, misalnya panggilan balik parametris yang disetel saat runtime di mana kompiler tidak dapat memonomorfisasi tubuh fungsi panggilan balik karena tidak diketahui pada waktu kompilasi. Tipe eksistensial seperti yang saya sebutkan sebelumnya setara dengan objek sifat Rust, yang merupakan salah satu cara untuk mencapai wadah heterogen dengan pengikatan nanti dalam Masalah Ekspresi daripada class mensubklasifikasikan warisan virtual, tetapi tidak terbuka untuk ekstensi dalam Ekspresi Masalah sebagai penyatuan dengan struktur data yang tidak dapat diubah atau penyalinan 3 yang memiliki biaya kinerja O(log n) .

1 Yang tidak memerlukan HKT dalam contoh di atas, karena SET tidak memerlukan elem type adalah parameter type dari tipe generik set , yaitu bukan set<elem> .

2 Namun jika terdapat lebih dari satu implementasi non-default dan tidak ada implementasi default, maka pemilihannya akan menjadi ambigu sehingga kompilator harus menghasilkan kesalahan.

3 Catatan bermutasi dengan struktur data yang tidak dapat diubah tidak perlu menyalin seluruh struktur data, jika struktur data cukup pintar untuk mengisolasi riwayat seperti daftar tertaut tunggal.

Menerapkan func pick(a CollectionOfT, count uint) []T akan menjadi contoh aplikasi obat generik yang baik (dari https://github.com/golang/go/issues/23717):

// pick returns a slice (len = n) of pseudorandomly chosen elements 
// in unspecified order from c which is an array, slice, or map.
for i, e := range pick(c, n) {

Pendekatan{} antarmuka di sini rumit.

Saya telah berkomentar beberapa kali tentang masalah ini bahwa salah satu masalah utama dengan pendekatan template C++ adalah ketergantungannya pada resolusi yang berlebihan sebagai mekanisme untuk metaprogramming waktu kompilasi.

Tampaknya Herb Sutter telah sampai pada kesimpulan yang sama: sekarang ada proposal menarik untuk pemrograman waktu kompilasi di C++ .

Ini memiliki beberapa elemen yang sama dengan paket Go reflect dan proposal saya sebelumnya untuk fungsi waktu kompilasi di Go .

Hai.
Saya telah menulis proposal untuk obat generik dengan batasan untuk Go. Anda bisa membacanya di sini . Mungkin dapat ditambahkan sebagai dokumen 15292. Ini sebagian besar tentang kendala dan dibaca sebagai amandemen Parameter Tipe Taylor di Go .
Ini dimaksudkan sebagai contoh cara yang bisa diterapkan (saya percaya) untuk melakukan obat generik 'ketik aman' di Go, - semoga dapat menambahkan sesuatu ke diskusi ini.
Harap dicatat, bahwa sementara saya telah membaca (sebagian besar) utas yang sangat panjang ini, saya belum mengikuti semua tautan di dalamnya, jadi orang lain mungkin telah membuat saran serupa. Jika itu masalahnya, saya minta maaf.

br. Kr.

Sintaks pelepasan sepeda:

constraint[T] Array {
    :[#]T
}

bisa jadi

type [T] Array constraint {
    _ [...]T
}

yang lebih mirip Go to me. :-)

Beberapa elemen di sini.

Satu hal adalah mengganti : dengan _ dan mengganti # dengan ... .
Saya kira Anda bisa melakukannya jika itu lebih disukai.

Hal lain adalah mengganti constraint[T] Array dengan type[T] Array constraint .
Itu sepertinya menunjukkan bahwa kendala adalah tipe, yang menurut saya tidak benar. Secara formal, kendala adalah _predikat_ pada himpunan semua jenis, yaitu. pemetaan dari set tipe ke set { true , false }.
Atau jika Anda mau, Anda dapat menganggap batasan hanya sebagai _a set of_ tipe.
Ini bukan tipe _a_.

br. Kr.

Mengapa constraint itu tidak menjadi interface saja?

type [T io.Writer] List struct { 
    element T; 
    next *List[T];
}

Antarmuka akan sedikit lebih berguna sebagai kendala dengan proposal berikut: #23796 yang pada gilirannya juga akan memberikan beberapa manfaat untuk proposal itu sendiri.

Juga, jika proposal untuk jenis jumlah diterima dalam beberapa bentuk (#19412), maka itu harus digunakan untuk membatasi jenisnya.

Meskipun saya percaya kata kunci kendala, beberapa sesuatu seperti itu harus ditambahkan, agar tidak mengulangi kendala besar dan mencegah kesalahan karena linglung.

Akhirnya, untuk bagian bikeshedding, saya pikir batasan harus dicantumkan di akhir definisi, untuk menghindari kepadatan yang berlebihan (sepertinya karat memiliki ide bagus di sini):

// similar to the map[T]... syntax
// also no constraint
type List[T] struct {
    element T
    next *List[T]
}

// with constraint
type List[T] struct {
    element T
    next *List[T]
} where T is io.Writer | encoding.BinaryMarshaler

type BigConstraint constraint {
     io.Writer
     SomeFunc() int
     AnotherFunc()
     AField int64
     StringField string
}


// with predefined constraint
type List[T, U] struct {
    element T
    val U
    next *List[T, U]
} where T is BigConstraint | encoding.BinaryMarshaler,
    U is io.Reader

@urandom : Saya pikir ini adalah satu keuntungan besar untuk memiliki antarmuka yang diimplementasikan secara implisit alih-alih secara eksplisit. @surlykke proposal dalam komentar ini saya pikir jauh lebih dekat dengan sintaks Go lainnya dalam semangat.

@surlykke Saya minta maaf jika proposal memiliki jawaban untuk semua ini.

Penggunaan generik adalah untuk memungkinkan fungsi gaya bawaan. Bagaimana Anda menerapkan len tingkat aplikasi dengan ini? Tata letak memori berbeda untuk setiap input yang diizinkan, jadi bagaimana ini lebih baik daripada antarmuka?

"Pilihan" yang dijelaskan sebelumnya memiliki masalah serupa di mana pengindeksan ke dalam peta vs pengindeksan ke dalam irisan berbeda. Dalam kasus peta jika ada konversi untuk mengiris terlebih dahulu maka kode pengambilan yang sama dapat digunakan, tetapi bagaimana ini dilakukan?

Koleksi adalah kegunaan lain:

// An unordered collection of comparable items.
type [T Comparable] Set []T

func (a Set) Diff(from Set) Set {
    // the implementation is the same as one with
    //     type Comparable interface { Equal(Comparable) bool }
    //     type Set []Comparable
}

// compile error
d := Set[int]{1, 2}.Diff(Set[string]{“abc”, “def”})

// Go 1, easier to read but runtime error
d := Set{1, 2}.Diff(Set{“abc”, “def”})

Untuk kasus tipe koleksi, saya tidak yakin ini adalah kemenangan besar atas obat generik Go 1 karena ada pengorbanan keterbacaan.

Saya setuju bahwa parameter tipe harus memiliki beberapa bentuk batasan. Jika tidak, kami akan mengulangi kesalahan template C++. Pertanyaannya adalah, seberapa ekspresifkah batasan-batasan itu?

Di satu sisi, kita hanya bisa menggunakan antarmuka. Tetapi seperti yang Anda tunjukkan, banyak pola yang berguna tidak dapat ditangkap dengan cara itu.

Lalu ada ide Anda, dan ide serupa, yang mencoba mengukir serangkaian batasan yang berguna dan menyediakan sintaks baru untuk mengekspresikannya. Selain masalah menambahkan lebih banyak sintaks, tidak jelas di mana harus berhenti. Seperti yang Anda tunjukkan, proposal Anda menangkap banyak pola, tetapi tidak semuanya.

Di ekstrem lainnya adalah ide yang saya usulkan dalam dokumen ini . Ia menggunakan kode Go itu sendiri sebagai bahasa kendala. Anda dapat menangkap hampir semua kendala dengan cara itu, dan tidak memerlukan sintaks baru.

@jba
Ini sedikit bertele-tele. Mungkin jika Go memiliki sintaks lambda, itu akan sedikit lebih enak. Di sisi lain, tampaknya masalah terbesar yang coba dipecahkannya adalah memeriksa apakah suatu tipe mendukung semacam operator. Mungkin akan lebih mudah jika Go memiliki antarmuka yang telah ditentukan sebelumnya untuk berbagai operator:

func equal[T](x, y T) bool
    where T is runtime.Equitable {
    return x == y
}

func copyable[T](x, y []T) int {
    return copy(x, y)
}

atau sesuatu di sepanjang garis ini.

Jika masalahnya adalah dengan memperluas builtin, maka mungkin masalahnya terletak pada cara bahasa membuat tipe adaptor. Misalnya, bukankah kembung terkait dengan sort.Interface adalah alasan utama di balik https://github.com/golang/go/issues/16721 dan sort.Slice?
Melihat https://github.com/golang/go/issues/21670#issuecomment -325739411 , gagasan @Sajmani untuk memiliki literal antarmuka mungkin merupakan bahan yang diperlukan untuk parameter tipe agar mudah bekerja dengan bawaan.
Perhatikan definisi Iterator berikut ini:

type [T] Iterator interface {
    Next() (elem T, done bool)
}

Jika print adalah fungsi yang hanya mengulangi daftar dan mencetak isinya, maka contoh berikut menggunakan literal antarmuka untuk membangun antarmuka yang memuaskan untuk print .

func SliceIterator(slice []T) Iterator {
    i := 0
    return Iterator{
        Next: func() (elem int, done bool) {
            v := slice[i]
            if i+1 == len(slice) {
                return v, true
            }
            i++
            return v, false
        },
    }
}

func main() {
    arr := []int{1,2,3,4,5}
    // SliceIterator works for an arbitrary slice
    print(SliceIterator(arr))
}

Seseorang sudah dapat melakukan ini jika mereka secara global mendeklarasikan tipe yang satu-satunya tanggung jawab adalah untuk memenuhi antarmuka. Namun, konversi dari fungsi ke metode ini membuat antarmuka (dan karenanya "kendala") lebih mudah dipenuhi. Kami tidak mencemari deklarasi tingkat atas dengan adaptor sederhana (seperti "widgetsByName" dalam penyortiran).
Tipe yang ditentukan pengguna jelas juga dapat memanfaatkan fitur ini, seperti yang terlihat pada contoh LinkedList ini:

type ListNode struct {
    v string
    next *ListNode
}
func (l *ListNode) Iterator() Iterator {
    ptr := l
    return Iterator{
        Next: func() (elem int, done bool) {
            v := ptr.v
            if ptr.next == nil {
                return v, true
            }
            ptr = ptr.next
            return v, false
        },
    }
}

@geovanisouza92 : Batasan seperti yang saya jelaskan lebih ekspresif daripada antarmuka (bidang, operator). Saya memang secara singkat mempertimbangkan untuk memperluas antarmuka alih-alih memperkenalkan batasan, tetapi saya pikir itu akan menjadi perubahan yang terlalu mengganggu pada elemen Go yang ada.

@ pciet Saya tidak yakin apa yang Anda maksud dengan 'tingkat aplikasi'. Go memiliki fungsi len bawaan yang dapat diterapkan ke array, penunjuk ke array, irisan, string, dan saluran, jadi, dalam proposal saya, jika parameter tipe dibatasi untuk memiliki salah satu dari ini sebagai tipe dasarnya , len dapat diterapkan padanya.

@pciet Tentang contoh Anda dengan batasan/antarmuka Comparable . Perhatikan bahwa jika Anda mendefinisikan (varian antarmuka):

type Comparable interface { Equal(Comparable) bool }
type Set []Comparable

Kemudian Anda dapat memasukkan apa pun yang mengimplementasikan Comparable ke dalam Set . Bandingkan dengan:

constraint [T] Comparable { Equal(t T) bool }
type [T Comparable[T]] Set []T
...
type FooSet Set[Foo] // Where Foo satisfies constraint Comparable

di mana Anda hanya dapat memasukkan nilai tipe Foo ke dalam FooSet . Itu adalah keamanan tipe yang lebih kuat.

@urandom Sekali lagi, saya bukan penggemar:

type MyConstraint constraint {....}

karena saya tidak percaya kendala adalah tipe. Juga, saya pasti tidak akan mengizinkan:

var myVar MyConstraint

yang tidak masuk akal bagi saya. Indikasi lain bahwa kendala bukanlah tipe.

@urandom Di bikeshedding: Saya percaya Kendala harus dideklarasikan tepat di sebelah parameter tipe. Pertimbangkan fungsi biasa, yang didefinisikan seperti ini:

func MyFunc(i) {
     if (i>0) fmt.Println("It's positive")
} with i being an integer

Anda tidak dapat membaca ini dari kiri ke kanan. Sebagai gantinya, Anda terlebih dahulu membaca func MyFunc(i) untuk menentukan bahwa itu adalah definisi fungsi. Kemudian Anda harus melompat ke akhir untuk mencari tahu apa itu i , dan kemudian kembali ke badan fungsi. Tidak ideal, IMO. Dan saya tidak melihat bagaimana definisi umum harus berbeda.
Tapi jelas, diskusi ini ortogonal dengan diskusi tentang apakah Go harus memiliki kendala atau generik.

@surlykke
Saya baik-baik saja dengan itu bukan tipe. Yang paling penting adalah mereka memiliki nama sehingga mereka dapat disebut dengan beberapa jenis.

Untuk fungsi, jika kita mengikuti sintaks karat, itu akan menjadi:

func MyFunc[I](i I) int64
     where I is being an integer {
   return 42
}

Jadi itu tidak akan menyembunyikan hal-hal seperti nama fungsi atau parameternya, dan Anda tidak perlu pergi ke akhir badan fungsi untuk melihat batasan apa pada tipe generiknya.

@surlykke untuk anak cucu, dapatkah Anda menemukan di mana proposal Anda dapat ditambahkan ke:
https://docs.google.com/document/d/1vrAy9gMpMoS3uaVphB32uVXX4pi-HnNjkMEgyAHX4N4

Ini adalah tempat yang bagus untuk "mengkompilasi" semua proposal.

Pertanyaan lain yang saya ajukan kepada Anda semua adalah bagaimana seseorang akan menangani spesialisasi berbagai instantiasi dari tipe generik. Dalam proposal tipe-params , cara melakukannya adalah dengan menghasilkan fungsi templat yang sama untuk setiap tipe yang dipakai, mengganti parameter tipe dengan nama tipe. Untuk memiliki fungsionalitas terpisah untuk tipe yang berbeda, lakukan sakelar tipe pada parameter tipe.

Apakah aman untuk mengasumsikan bahwa ketika kompiler melihat sakelar tipe pada parameter tipe, itu diizinkan untuk menghasilkan implementasi terpisah untuk setiap pernyataan? Atau apakah itu terlalu melibatkan pengoptimalan, karena parameter tipe bersarang dalam struct yang ditegaskan dapat membuat aspek parametrik pada pembuatan kode?

Dalam proposal fungsi waktu kompilasi , karena kita tahu bahwa deklarasi ini dihasilkan pada waktu kompilasi, sakelar tipe tidak menimbulkan biaya runtime.

Skenario praktis: Jika kita mempertimbangkan kasus paket math/bits , melakukan penegasan tipe untuk memanggil OnesCount untuk setiap uintXX akan mengalahkan tujuan memiliki perpustakaan manipulasi bit yang efisien. Namun, jika tipe-pernyataan diubah menjadi berikut:

func OnesCount(x T) int {
    switch x.(type) {
    case uint:
        // separate uint functionality...
    case uint8:
        // separate uint8 functionality...
    case uint16:
        // separate uint16 functionality...
    case uint32:
        // separate uint32 functionality...
    case uint64:
        // separate uint64 functionality...
    }
}

Panggilan untuk

var x uint8 = 255
bits.OnesCount(x)

kemudian akan memanggil fungsi yang dihasilkan berikut (nama tidak penting di sini):

func $OnesCount_uint8(x uint8) {
    // separate uint8 functionality...
}

@jba Itu proposal yang menarik, tetapi bagi saya itu sebagian besar menyoroti fakta bahwa definisi fungsi parametrik itu sendiri biasanya cukup untuk mendefinisikan batasannya.

Jika Anda akan menggunakan "operator yang digunakan dalam suatu fungsi" sebagai batasan, lalu keuntungan apa yang Anda dapatkan untuk menulis fungsi kedua yang berisi subset dari operator yang digunakan di fungsi pertama?

@bcmils Salah satunya adalah spesifikasi dan yang lainnya adalah implementasinya. Ini adalah keuntungan yang sama dengan pengetikan statis: Anda dapat menangkap kesalahan lebih awal.

Jika implementasinya adalah spesifikasi, la C++ template, maka setiap perubahan pada implementasi berpotensi merusak tanggungan. Itu mungkin tidak ditemukan sampai lama kemudian, ketika tanggungan dikompilasi ulang, dan penemu tidak memiliki konteks untuk memahami pesan kesalahan. Dengan spesifikasi dalam paket yang sama, Anda dapat mendeteksi kerusakan secara lokal.

@mandolyte Saya tidak yakin di mana harus menambahkannya - mungkin paragraf di bawah 'Pendekatan generik' bernama 'Generik dengan kendala'?
Dokumen tersebut tampaknya tidak memuat banyak tentang batasan jenis parameter, jadi jika Anda menambahkan paragraf di mana proposal saya akan disebutkan, maka pendekatan lain untuk batasan dapat dicantumkan di sana juga.

@surlykke pendekatan umum pada dokumen adalah membuat perubahan apa yang terasa benar dan saya akan mencoba menerima, menggabungkan, dan mengaturnya dengan sisa dokumen. Saya menambahkan bagian di sini . Jangan ragu untuk menambahkan hal-hal yang saya lewatkan.

@egonelbre Itu sangat bagus. Terima kasih!

@jba
Saya suka proposal Anda, tapi saya pikir itu terlalu berat untuk golang. Itu mengingatkan saya banyak template di c++. Masalah utama yang saya pikir adalah Anda dapat menulis kode yang sangat kompleks dengannya.
Untuk memutuskan apakah dua instance antarmuka generik tumpang tindih karena kumpulan tipe yang dibatasi tumpang tindih akan menjadi tugas yang sulit yang menyebabkan waktu kompilasi lebih lambat. Hal yang sama untuk pembuatan kode.

Saya pikir batasan yang diusulkan lebih ringan untuk digunakan. Dari apa yang saya dengar adalah bahwa kendala alias kelas tipe dapat diimplementasikan secara ortogonal ke sistem tipe suatu bahasa.

Saya harus sangat setuju bahwa kita tidak boleh menggunakan batasan implisit dari tubuh fungsi. Mereka secara luas dianggap sebagai salah satu kesalahan paling signifikan dari template C++:

  • Kendala tidak mudah terlihat. Sementara godoc secara teoritis dapat menghitung semua kendala ke dalam dokumentasi, mereka tidak terlihat dalam kode sumber kecuali secara implisit.
  • Karena itu, ada kemungkinan untuk secara tidak sengaja menyertakan batasan tambahan yang hanya terlihat saat Anda mencoba menggunakan fungsi dengan cara yang tidak diharapkan. Dengan mensyaratkan spesifikasi eksplisit dari kendala, programmer harus tahu persis kendala apa yang mereka perkenalkan.
  • Itu membuat keputusan tentang jenis kendala apa yang diizinkan jauh lebih ad-hoc. Misalnya, apakah saya diizinkan untuk mendefinisikan fungsi berikut? Apa kendala sebenarnya pada T, U, dan V di sini? Jika kami mengharuskan programmer untuk secara eksplisit menentukan batasan, maka kami konservatif dalam jenis batasan yang kami izinkan (membiarkan kami memperluasnya secara perlahan dan sengaja). Jika kita mencoba untuk tetap konservatif, bagaimana kita memberikan pesan kesalahan untuk fungsi seperti ini? "Kesalahan: tidak dapat menetapkan uv() ke T karena memberlakukan batasan ilegal"?
func[T, U, V] Foo(u U, v V) {
  var t T = u.v(V) + 1;
}
  • Memanggil fungsi generik dalam fungsi generik lainnya memperburuk situasi di atas, karena Anda sekarang perlu melihat semua batasan yang dipanggil untuk memahami batasan fungsi yang Anda tulis atau baca.
  • Debugging bisa sangat sulit, karena pesan kesalahan harus tidak memberikan informasi yang cukup untuk menemukan sumber kendala, atau harus membocorkan detail internal fungsi. Misalnya, jika F memiliki beberapa persyaratan pada tipe T , dan penulis F mencoba mencari tahu dari mana persyaratan itu berasal, mereka ingin kompilernya beri tahu mereka dengan tepat pernyataan mana yang menimbulkan kendala (terutama jika itu berasal dari panggilan umum). Tetapi pengguna F tidak menginginkan informasi itu dan, memang, jika disertakan dalam pesan kesalahan, maka kami membocorkan detail implementasi F dalam pesan kesalahan dari penggunanya, yang adalah pengalaman pengguna yang mengerikan.

@alercah

Misalnya, apakah saya diizinkan untuk mendefinisikan fungsi berikut?

func[T, U, V] Foo(u U, v V) {
  var t T = u.v(V) + 1;
}

Tidak. u.v(V) adalah kesalahan sintaks karena V adalah tipe, dan variabel t tidak digunakan.

Namun, Anda dapat mendefinisikan fungsi ini, yang mungkin merupakan fungsi yang Anda inginkan:

func[T, U, V] Foo(u U, v V) {
    var _ T = u.v(v) + 1;
}

Apa kendala sebenarnya pada T, U, dan V di sini?

  • Jenis V tidak dibatasi.
  • Jenis U harus memiliki metode v yang menerima parameter tunggal atau varargs dari beberapa jenis yang dapat ditetapkan dari V , karena u.v dipanggil dengan satu argumen dari jenis V .

    • U.v bisa menjadi bidang tipe fungsi, tetapi bisa dibilang itu harus menyiratkan metode; lihat #23796.

  • Jenis yang dikembalikan oleh U.v harus numerik, karena konstanta 1 ditambahkan ke dalamnya.
  • Tipe kembalian U.v harus dapat ditetapkan ke T , karena u.v(…) + 1 ditetapkan ke variabel tipe T .
  • Jenis T harus numerik, karena jenis kembalian U.v adalah numerik dan dapat ditetapkan ke T .

(Selain: Anda dapat berargumen bahwa U dan V harus memiliki batasan "dapat disalin" karena argumen dari tipe tersebut dilewatkan oleh nilai, tetapi sistem tipe non-generik yang ada tidak menegakkan kendala itu juga. Itu masalah untuk proposal terpisah.)

Jika kami mengharuskan programmer untuk secara eksplisit menentukan batasan, maka kami konservatif dalam jenis batasan yang kami izinkan (membiarkan kami memperluasnya secara perlahan dan sengaja).

Ya, itu benar: tetapi menghilangkan kendala akan menjadi cacat serius apakah kendala itu tersirat atau tidak. IMO, peran kendala yang lebih penting adalah menyelesaikan ambiguitas. Misalnya, dalam batasan di atas, kompiler harus siap untuk membuat instance u.v sebagai metode argumen tunggal atau variadik.

Ambiguitas yang paling menarik terjadi untuk literal, di mana kita perlu membedakan antara tipe struct dan tipe komposit:

func[T] Foo() (t T) {
    x := 42;
    t = T{x: "some string"}  // Is x an index, or a field name?
    _ = x
}

Jika kita mencoba untuk tetap konservatif, bagaimana kita memberikan pesan kesalahan untuk fungsi seperti ini? "Kesalahan: tidak dapat menetapkan uv() ke T karena memberlakukan batasan ilegal"?

Saya tidak yakin apa yang Anda tanyakan, karena saya tidak melihat kendala yang saling bertentangan untuk contoh ini. Apa yang Anda maksud dengan "pembatasan ilegal"?

Debugging bisa sangat sulit, karena pesan kesalahan harus tidak memberikan informasi yang cukup untuk menemukan sumber kendala, atau harus membocorkan detail internal fungsi.

Tidak setiap batasan yang relevan dapat diekspresikan oleh sistem tipe (lihat juga https://github.com/golang/go/issues/22876#issuecomment-347035323). Beberapa kendala diberlakukan oleh kepanikan run-time; beberapa ditegakkan oleh detektor balapan; kendala paling berbahaya hanya didokumentasikan dan tidak terdeteksi sama sekali.

Semua "kebocoran detail internal" itu sampai taraf tertentu. (Lihat juga https://xkcd.com/1172/.)

Misalnya, jika […] penulis F mencoba mencari tahu dari mana persyaratan itu berasal, mereka ingin kompiler memperingatkan mereka tentang pernyataan mana yang menimbulkan kendala (terutama jika itu berasal dari panggilan umum). Tetapi pengguna F tidak menginginkan informasi itu[.]

Mungkin? Begitulah cara penulis API menggunakan anotasi tipe dalam bahasa yang disimpulkan dengan tipe seperti Haskell dan ML, tetapi juga mengarah ke lubang kelinci tipe parametrik (“tingkat tinggi”) secara umum.

Misalnya, Anda memiliki fungsi ini:

func [F, Arg, Result] InvokeAsync(f F, x Arg) (<-chan Result) {
    c := make(chan result, 1)
    go func() { c <- f(x) }()
    return c
}

Bagaimana Anda mengekspresikan batasan eksplisit pada tipe Arg ? Mereka bergantung pada instantiasi spesifik F . Ketergantungan semacam itu tampaknya hilang dari banyak proposal kendala baru-baru ini.

Tidak. uv(V) adalah kesalahan sintaks karena V adalah tipe, dan variabel t tidak digunakan.

Namun, Anda dapat mendefinisikan fungsi ini, yang mungkin merupakan fungsi yang Anda inginkan:

Ya, itu tujuannya, saya minta maaf.

Jenis T harus numerik, karena jenis kembalian U.v adalah numerik dan dapat ditetapkan ke T .

Haruskah kita benar-benar menganggap ini sebagai kendala? Ini dapat dikurangkan dari batasan lain, tetapi apakah lebih atau kurang berguna untuk menyebut ini sebagai batasan yang berbeda? Kendala implisit mengajukan pertanyaan ini dengan cara yang tidak dilakukan oleh kendala eksplisit.

Ya, itu benar: tetapi menghilangkan kendala akan menjadi cacat serius apakah kendala itu tersirat atau tidak. IMO, peran kendala yang lebih penting adalah menyelesaikan ambiguitas. Misalnya, dalam batasan di atas, kompiler harus siap untuk membuat instance uv baik sebagai argumen tunggal atau metode variadik.

Maksud saya "batasan yang kami izinkan" seperti dalam bahasa. Dengan batasan eksplisit, jauh lebih mudah bagi kita untuk memutuskan batasan seperti apa yang ingin kita izinkan pengguna untuk menulis, daripada hanya mengatakan batasannya adalah "apa pun yang membuat sesuatu dikompilasi". Misalnya, contoh saya Foo di atas sebenarnya melibatkan tipe tambahan implisit yang terpisah dari T , U , atau V , karena kita harus mempertimbangkan tipe kembalian dari u.v . Jenis ini tidak secara eksplisit dirujuk dengan cara apa pun dalam deklarasi f ; properti yang harus dimiliki sepenuhnya implisit. Demikian juga, apakah kami bersedia mengizinkan jenis dengan peringkat lebih tinggi ( forall )? Saya tidak dapat memberikan contoh dari atas kepala saya, tetapi saya juga tidak dapat meyakinkan diri sendiri bahwa Anda tidak dapat secara implisit menulis tipe terikat dengan peringkat lebih tinggi.

Contoh lain adalah apakah kita harus mengizinkan suatu fungsi untuk memanfaatkan sintaks yang kelebihan beban. Jika fungsi yang dibatasi secara implisit melakukan for i := range t untuk beberapa t dari tipe generik T , sintaksnya berfungsi jika T adalah array, irisan, saluran, atau peta. Tetapi semantiknya sangat berbeda, terutama jika T adalah jenis saluran. Misalnya, jika t == nil (yang dapat terjadi selama T adalah array), maka iterasi tidak menghasilkan apa-apa, karena tidak ada elemen dalam irisan atau peta nihil, atau blok selamanya karena itulah yang diterima di saluran nil . Ini adalah senjata besar yang menunggu untuk terjadi. Demikian pula yang dilakukan m[i] = ... ; jika saya bermaksud m menjadi peta, saya harus menjaganya agar tidak benar-benar menjadi irisan karena kode bisa panik pada tugas di luar jangkauan jika tidak.

Faktanya, saya pikir ini cocok untuk argumen lain melawan batasan implisit: penulis API mungkin menulis pernyataan buatan hanya untuk menambahkan batasan. Misalnya for _, _ := range t { break } mencegah saluran sambil tetap mengizinkan peta, irisan, dan larik; x = append(x) memaksa x untuk memiliki tipe irisan. var _ = make(T, 0) memungkinkan irisan, peta, dan saluran tetapi tidak array. Akan ada buku resep tentang cara menambahkan batasan secara implisit sehingga seseorang tidak dapat memanggil fungsi Anda dengan tipe yang kodenya tidak Anda tulis dengan benar. Saya bahkan tidak bisa memikirkan cara untuk menulis kode yang hanya dikompilasi untuk tipe peta kecuali saya tahu tipe kuncinya juga. Dan saya tidak berpikir ini hipotetis sama sekali; peta dan irisan berperilaku sangat berbeda untuk sebagian besar aplikasi

Saya tidak yakin apa yang Anda tanyakan, karena saya tidak melihat kendala yang saling bertentangan untuk contoh ini. Apa yang Anda maksud dengan "pembatasan ilegal"?

Maksud saya batasan yang tidak diizinkan oleh bahasa, seperti jika bahasa memutuskan untuk melarang batasan peringkat yang lebih tinggi.

Tidak setiap batasan yang relevan dapat diekspresikan oleh sistem tipe (lihat juga #22876 (komentar)). Beberapa kendala diberlakukan oleh kepanikan run-time; beberapa ditegakkan oleh detektor balapan; kendala paling berbahaya hanya didokumentasikan dan tidak terdeteksi sama sekali.

Semua "kebocoran detail internal" itu sampai taraf tertentu. (Lihat juga https://xkcd.com/1172/.)

Saya tidak benar-benar melihat bagaimana #22876 masuk ke dalam ini; yang mencoba menggunakan sistem tipe untuk mengekspresikan jenis batasan yang berbeda. Akan selalu benar bahwa kita tidak dapat mengungkapkan beberapa batasan pada nilai, atau pada program, bahkan dengan sistem tipe dengan kompleksitas arbitrer. Tapi kita hanya berbicara tentang batasan pada tipe di sini. Kompiler harus dapat menjawab pertanyaan "Dapatkah saya membuat instance generik ini dengan tipe T ?" yang berarti bahwa ia harus memahami batasan, apakah itu implisit atau eksplisit. (Perhatikan bahwa beberapa bahasa, seperti C++ dan Rust, tidak dapat memutuskan pertanyaan ini secara umum karena dapat bergantung pada komputasi arbitrer dan dengan demikian beralih ke Masalah Penghentian, tetapi mereka masih mengungkapkan batasan yang perlu dipenuhi.)

Maksud saya lebih seperti "pesan kesalahan apa yang harus diberikan oleh contoh berikut?"

func [U] DirectlyConstrained(U t) {
    t.DoSomething();
}
func [T] IndirectlyConstrained(T t) {
    DirectlyConstrainted(t);
}
func Illegal() {
    IndirectlyConstrained(4);
}

Kita dapat mengatakan Error: cannot call IndirectlyConstrained with [T = int]; T must have a method with signature func (T t) DoSomething() . Pesan kesalahan ini berguna bagi pengguna IndirectlyConstrained , karena dengan jelas menetapkan batasan yang tidak ada. Tetapi itu tidak memberikan informasi kepada seseorang yang mencoba men-debug mengapa IndirectlyConstrained memiliki batasan itu, yang merupakan masalah kegunaan besar jika itu adalah fungsi yang besar. Kami dapat menambahkan Note: this constraint exists on T because IndirectlyConstrained calls DirectlyConstrained with [U = T] on line N , tetapi sekarang kami membocorkan detail implementasi IndirectlyConstrained . Selanjutnya, kami belum menjelaskan mengapa IndirectlyConstrained memiliki batasan, jadi apakah kami menambahkan Note: this constraint exists on U because DirectlyConstrained calls t.DoSomething() on line M lain? Bagaimana jika batasan implisit berasal dari beberapa callee empat tingkat di bawah tumpukan panggilan?

Selanjutnya, bagaimana kita memformat pesan kesalahan ini untuk jenis yang tidak secara eksplisit terdaftar sebagai parameter? Misalnya jika dalam contoh di atas, IndirectlyConstrained memanggil DirectlyConstrained(t.U()) . Bagaimana kita merujuk ke jenisnya? Dalam hal ini kita dapat mengatakan the type of t.U() , tetapi nilainya tidak selalu merupakan hasil dari satu ekspresi; itu bisa dibangun di atas beberapa pernyataan. Kemudian kita perlu menyintesis ekspresi dengan tipe yang benar untuk dimasukkan ke dalam pesan kesalahan, yang tidak pernah muncul dalam kode, atau kita perlu menemukan cara lain untuk merujuknya yang akan kurang jelas bagi penelepon miskin yang melanggar batasan.

Bagaimana Anda mengekspresikan batasan eksplisit pada tipe Arg? Mereka bergantung pada instantiasi spesifik F. Ketergantungan semacam itu tampaknya hilang dari banyak proposal batasan baru-baru ini.

Jatuhkan F dan miliki tipe f menjadi func (Arg) Result . Ya, itu mengabaikan fungsi variadik, tetapi Go lainnya juga melakukannya. Proposal untuk membuat varargs funcs dapat dialihkan ke tanda tangan yang kompatibel dapat dilakukan secara terpisah.

Untuk kasus di mana kami benar-benar memerlukan batas jenis orde tinggi, mungkin masuk akal atau tidak untuk memasukkannya ke dalam generik v1. Kendala eksplisit memang memaksa kita untuk memutuskan secara eksplisit apakah kita ingin mendukung tipe tingkat tinggi, dan bagaimana caranya. Kurangnya pertimbangan sejauh ini adalah gejala, menurut saya, dari fakta bahwa Go saat ini tidak memiliki cara untuk merujuk ke properti tipe bawaan. Ini adalah pertanyaan umum yang terbuka tentang bagaimana sistem generik mana pun akan mengizinkan fungsi generik di semua tipe numerik, atau semua tipe integer, dan sebagian besar proposal belum terlalu fokus pada hal ini.

Silakan evaluasi implementasi obat generik saya di proyek Anda berikutnya
http://go-li.github.io/

Kita dapat mengatakan Error: cannot call IndirectlyConstrained with [T = int]; T must have a method with signature func (T t) DoSomething() . Pesan kesalahan ini […] tidak memberikan informasi kepada seseorang yang mencoba men-debug mengapa IndirectlyConstrained memiliki batasan itu, yang merupakan masalah kegunaan besar jika itu adalah fungsi yang besar.

Saya ingin menunjukkan asumsi besar yang Anda buat di sini: bahwa pesan kesalahan dari go build adalah alat _only_ yang tersedia bagi pemrogram untuk mendiagnosis masalah.

Untuk menggunakan analogi: jika Anda menemukan error saat run-time, Anda memiliki beberapa opsi untuk debugging. Kesalahan itu sendiri hanya berisi pesan sederhana, yang mungkin atau mungkin tidak cukup untuk menggambarkan kesalahan. Tapi itu bukan satu-satunya informasi yang Anda miliki: misalnya, Anda juga memiliki pernyataan log apa pun yang dikeluarkan program, dan jika itu adalah bug yang sangat buruk, Anda dapat memuatnya ke dalam debugger interaktif.

Artinya, run-time debugging adalah proses interaktif. Jadi mengapa kita harus mengasumsikan debugging non-interaktif untuk kesalahan waktu kompilasi? Sebagai salah satu alternatif, kita dapat mengajarkan alat guru tentang batasan tipe. Kemudian, output dari kompiler akan menjadi seperti:

somefile.go:123: Argument `4` to DirectlyConstrained has type `int`,
    but DirectlyConstrained requires a type `T` with method `DoSomething()`.
    (For more detail, run `guru contraints path/to/somefile.go:#1033`.)

Itu memberi pengguna paket generik informasi yang mereka butuhkan untuk men-debug situs panggilan langsung, tetapi _also_ memberikan remah roti untuk pengelola paket (dan, yang penting, lingkungan pengeditan mereka!) untuk menyelidiki lebih lanjut.

Kami dapat menambahkan Note: this constraint exists on T because IndirectlyConstrained calls DirectlyConstrained with [U = T] on line N , tetapi sekarang kami membocorkan detail implementasi IndirectlyConstrained .

Ya, itulah yang saya maksud tentang bocornya informasi. Anda sudah dapat menggunakan guru describe untuk mengintip ke dalam implementasi. Anda dapat mengintip ke dalam program yang sedang berjalan menggunakan debugger, dan tidak hanya mencari tumpukan tetapi juga turun ke fungsi tingkat rendah yang sewenang-wenang.

Saya sangat setuju bahwa kita harus menyembunyikan informasi yang mungkin tidak relevan _secara default_, tetapi itu tidak berarti bahwa kita harus menyembunyikannya secara mutlak.

Jika fungsi yang dibatasi secara implisit berfungsi untuk i := range t untuk beberapa t dari tipe generik T , sintaksnya berfungsi jika T adalah array, irisan, saluran apa pun , atau peta. Tetapi semantiknya sangat berbeda, terutama jika T adalah jenis saluran.

Saya pikir itu argumen yang lebih menarik untuk batasan tipe, tetapi itu tidak memerlukan batasan eksplisit untuk mendekati verbose seperti yang diusulkan beberapa orang. Untuk membedakan situs panggilan, tampaknya cukup untuk membatasi parameter tipe dengan sesuatu yang lebih dekat ke reflect.Kind . Kita tidak perlu menjelaskan operasi yang sudah jelas dari kode; sebagai gantinya, kita hanya perlu mengatakan hal-hal seperti " T is a slice type". Itu mengarah ke serangkaian batasan yang lebih sederhana:

  • jenis yang tunduk pada operasi indeks perlu diberi label sebagai linier atau asosiatif,
  • jenis yang tunduk pada range operasi perlu diberi label sebagai nil-kosong atau nil-blocking,
  • tipe dengan literal perlu diberi label memiliki bidang atau indeks, dan
  • (mungkin) tipe dengan operasi numerik perlu diberi label sebagai titik tetap atau mengambang.

Itu mengarah ke bahasa batasan yang jauh lebih sempit, mungkin seperti:

TypeConstraint = "sliceable" | "map" | "chan" | "struct" | "integer" | "float" | "type"

dengan contoh seperti:

func[T:integer, U, V] Foo(u U, v V) {
    var _ T = u.v(v) + 1;
}
func [S:sliceable, T] append(s S, x ...T) S {
    dst := s
    if cap(s) - len(s) < len(x) {
        dst = make(S, len(s), nextSizeClass(cap(s)))
        copy(dst, s)
    }
    copy(dst[len(s):cap(s)], x)
    return dst[:len(s)+len(x)]
}

Saya merasa kami telah mengambil langkah besar menuju generik khusus dengan memperkenalkan alias tipe.
Tipe alias memungkinkan tipe super (tipe tipe).
Kita dapat memperlakukan tipe seperti nilai dalam menggunakan.

Untuk mempermudah penjelasan, kita dapat menambahkan elemen kode baru, genre .
Hubungan antara genre dan tipe seperti hubungan antara tipe dan nilai.
Dengan kata lain, genre berarti jenis jenis.

Setiap jenis tipe, kecuali tipe struct dan antarmuka dan fungsi, sesuai dengan genre yang telah dideklarasikan sebelumnya.

  • Bool
  • Rangkaian
  • Int8, Uint8, Int16, Uint16, Int32, Uint32, Int64, Uint64, Int, Uint, Uintptr
  • Float32, Float64
  • Kompleks64, Kompleks128
  • Array, Iris, Peta, Saluran, Pointer, UnsafePointer

Ada beberapa genre yang telah dideklarasikan sebelumnya, seperti Comaprable, Numeric, Interger, Float, Complex, Container, dll. Kita dapat menggunakan Type atau * untuk menunjukkan semua jenis genre.

Nama semua genre bawaan semuanya dimulai dengan huruf besar.

Setiap struct dan antarmuka dan tipe fungsi sesuai dengan genre.

Kami juga dapat mendeklarasikan genre khusus:

genre Addable = Numeric | String
genre Orderable = Interger | Float | String
genre Validator = func(int) bool // each parameter and result type must be a specified type.
genre HaveFieldsAndMethods = {
    width  int // we must use a specific type to define the fields.
    height int // we can't use a genre to define the fields.
    Load(v []byte) error // each parameter and result type must be a specified type.
    DoSomthing()
}
genre GenreFromStruct = aStructType // declare a genre from a struct type
genre GenreFromInterface = anInterfaceType // declare a genre from an interface type
genre GenreFromStructInterface = aStructType + anInterfaceType
genre ComparableStruct = HaveFieldsAndMethods & Comprable
genre UncomparableStruct = HaveFieldsAndMethods &^ Comprable

Untuk membuat penjelasan berikut konsisten, pengubah genre diperlukan.
Pengubah genre dilambangkan dengan Const . Sebagai contoh:

  • Const Integer adalah genre (berbeda dari Integer ) dan turunannya harus berupa nilai konstan yang tipenya harus bilangan bulat. Namun, nilai konstan dapat dilihat sebagai tipe khusus.
  • Const func(int) bool adalah genre (berbeda dari func(int) bool ) dan turunannya harus berupa nilai fungsi yang dihapus. Namun, deklarasi fungsi dapat dilihat sebagai tipe khusus.

(Solusi pengubah agak rumit, mungkin ada solusi desain lain yang lebih baik.)

Oke, mari kita lanjutkan.
Kami membutuhkan konsep lain. Menemukan nama yang baik untuk itu tidaklah mudah,
Sebut saja crate .
Secara umum, hubungan antara peti dan genre seperti hubungan antara fungsi dan jenis.
Peti dapat mengambil tipe sebagai parameter dan tipe pengembalian.

Deklarasi peti (anggap kode berikut dideklarasikan dalam paket lib ):

crate Example [T Float, S {width, height T}, N Const Integer] [*, *, *] {
    type MyArray [N]T

    func Add(a, b T) T {
        return a+b
    }

    type M struct {
        x T
        y S
    }

    func (m *M) Area() T {
        m.DoSomthing()
        return m.y.width * m.y.height
    }

    func (m *M) Perimeter() T {
        return 2 * Add(m.y.width, m.y.height)
    }

    export M, Add, MyArray
}

Menggunakan peti di atas.

import "lib"

// We can use AddFunc as a normal delcared function.
// Its genre is "Const func (a, b T) T"
type Rect, AddFunc, Array = lib.Example[float32, struct{x, y float32}, 100]

func demo() {
    var r Rect
    a, p = r.Area(), r.Perimeter()
    _ = AddFunc(a, p)
}

Ide-ide saya menyerap banyak ide-ide orang lain yang ditunjukkan di atas.
Mereka sangat tidak dewasa sekarang.
Saya mempostingnya di sini hanya karena saya merasa itu menarik,
dan saya tidak ingin memperbaikinya lagi.
Begitu banyak sel otak yang terbunuh dengan memperbaiki lubang-lubang ide.
Saya berharap ide-ide ini dapat membawa beberapa inspirasi untuk menghubungkan lainnya.

Apa yang Anda sebut "genre" sebenarnya disebut "jenis", dan terkenal di
komunitas pemrograman fungsional. Apa yang Anda sebut peti adalah terlarang
jenis fungsi ML.

Pada Rabu, 4 April 2018, 12:41, dotaheor [email protected] menulis:

Saya merasa kami telah mengambil langkah besar menuju generik khusus dengan memperkenalkan
ketik alias.
Tipe alias memungkinkan tipe super (tipe tipe).
Kita dapat memperlakukan tipe seperti nilai dalam menggunakan.

Untuk mempermudah penjelasan, kita dapat menambahkan elemen kode baru, genre.
Hubungan antara genre dan tipe seperti hubungan antar tipe
dan nilai-nilai.
Dengan kata lain, genre berarti jenis jenis.

Setiap jenis jenis, kecuali jenis struct dan antarmuka dan fungsi,
sesuai dengan genre yang telah dideklarasikan.

  • Bool
  • Rangkaian
  • Int8, Uint8, Int16, Uint16, Int32, Uint32, Int64, Uint64, Int, Uint,
    Uintptr
    & Float32, Float64
  • Kompleks64, Kompleks128
  • Array, Iris, Peta, Saluran, Pointer, UnsafePointer

Ada beberapa genre yang telah dideklarasikan sebelumnya, seperti Comaprable, Numeric,
Interger, Float, Complex, Container, dll. Kita bisa menggunakan Type atau * menunjukkan
genre semua jenis.

Nama semua genre bawaan semuanya dimulai dengan huruf besar.

Setiap struct dan antarmuka dan tipe fungsi sesuai dengan genre.

Kami juga dapat mendeklarasikan genre khusus:

genre Dapat ditambahkan = Numerik | Rangkaian
genre Dapat Diurutkan = Interger | mengambang | Rangkaian
genre Validator = func(int) bool // setiap parameter dan tipe hasil harus tipe yang ditentukan.
genre HaveFieldsAndMethods = {
width int // kita harus menggunakan tipe tertentu untuk mendefinisikan field.
height int // kita tidak bisa menggunakan genre untuk mendefinisikan field.
Load(v []byte) error // setiap parameter dan tipe hasil harus tipe yang ditentukan.
melakukan sesuatu()
}
genre GenreFromStruct = aStructType // mendeklarasikan genre dari tipe struct
genre GenreFromInterface = anInterfaceType // mendeklarasikan genre dari tipe antarmuka
genre GenreFromStructInterface = aStructType | sebuahInterfaceType

Untuk membuat penjelasan berikut konsisten, pengubah genre diperlukan.
Pengubah genre dilambangkan dengan Const. Sebagai contoh:

  • Const Integer adalah genre dan instance-nya harus berupa nilai konstan
    tipe mana yang harus berupa bilangan bulat.
    Namun, nilai konstan dapat dilihat sebagai tipe khusus.
  • Const func(int) bool adalah genre dan instance-nya harus delcared
    nilai fungsi.
    Namun, deklarasi fungsi dapat dilihat sebagai tipe khusus.

(Solusi pengubah agak rumit, mungkin ada desain lain yang lebih baik
solusi.)

Oke, mari kita lanjutkan.
Kami membutuhkan konsep lain. Menemukan nama yang baik untuk itu tidaklah mudah,
Sebut saja peti.
Secara umum, hubungan antara peti dan genre seperti hubungan
antara fungsi dan jenis.
Peti dapat mengambil tipe sebagai parameter dan tipe pengembalian.

Deklarasi peti (anggap kode berikut dideklarasikan di lib
kemasan):

peti Contoh [T Float, S {lebar, tinggi T}, N Const Integer] [*, *, *] {
ketik MyArray [N]T

func Tambah(a, b T) T {
kembali a+b
}

// Genre lingkup peti. Hanya bisa digunakan di peti.

// M adalah jenis genre G
tipe M struktur {
x T
y S
}

func (m *M) Luas() T {
m.Melakukan Sesuatu()
kembalikan lebarku * tinggiku
}

func (m *M) Keliling() T {
kembali 2 * Tambahkan (lebarku, tinggiku)
}

ekspor M, Tambah, MyArray
}

Menggunakan peti di atas.

impor "lib"

// Kita bisa menggunakan AddFunc sebagai fungsi delcared normal.
ketik Rect, AddFunc, Array = lib.Contoh(float32, struct{x, y float32})

fungsi demo() {
var r Rect
a, p = r.Area(), r.Perimeter()
_ = AddFunc(a, p)
}

Ide-ide saya menyerap banyak ide-ide orang lain yang ditunjukkan di atas.
Mereka sangat tidak dewasa sekarang.
Saya mempostingnya di sini hanya karena saya merasa itu menarik,
dan saya tidak ingin memperbaikinya lagi.
Begitu banyak sel otak yang terbunuh dengan memperbaiki lubang-lubang ide.


Anda menerima ini karena Anda disebutkan.
Balas email ini secara langsung, lihat di GitHub
https://github.com/golang/go/issues/15292#issuecomment-378665695 , atau bisukan
benang
https://github.com/notifications/unsubscribe-auth/AGGWB78BrjN0BxRfroH-jRNy4mCXgSwCks5tlPfMgaJpZM4IG-xv
.

Saya merasa ada beberapa perbedaan antara Jenis dan Genre.

Omong-omong, jika peti hanya mengembalikan satu jenis, kita dapat menggunakan panggilannya sebagai jenis secara langsung.

package lib

// export a type
crate List [T *] * {
    type List struct {
        ...
    }

    export List
}

Gunakan:

import "lib"

var l lib.List[int]

Akan ada beberapa aturan "pengurangan genre", seperti "pengurangan jenis" dalam sistem saat ini.

@dotaheor , @DemiMarie benar. Konsep "genre" Anda terdengar persis seperti "jenis" dari teori tipe. (Proposal Anda kebetulan memerlukan aturan subkinding, tapi itu tidak biasa.)

Kata kunci genre dalam proposal Anda mendefinisikan jenis baru sebagai jenis super dari jenis yang sudah ada. Kata kunci crate mendefinisikan objek dengan "tanda tangan peti", yang merupakan jenis yang bukan subjenis Type .

Sebagai sistem formal, proposal Anda tampaknya seperti:

Peti ::= χ | ⋯.
Ketik ::= τ | χ | int | bool | | func(τ) | func(τ) τ | []τ | χ[τ₁, …]

CrateSig ::= [κ₁, …] ⇒ [κₙ, …]
Baik ::= κ | exactly τ | kindOf κ | Map | Chan | | Const κ | Type | PetiSig

Untuk menyalahgunakan beberapa notasi teori tipe:

  • Baca "⊢" sebagai "membutuhkan".
  • Baca “ k1 k2 ” sebagai “ k1 adalah subjenis dari k2 ”.
  • Baca ":" sebagai "baik hati".

Kemudian aturannya terlihat seperti:

τ : exactly τ
exactly τ kindOf exactly τ
kindOf exactly τ Type

τ : κ₁κ₁κ₂ τ : κ₂

τ₁ : Type τ₂ : Type kindOf exactly map[τ₁]τ₂ Map
Map Type

κ₁ κ₂ Const κ₁ Const κ₂

[…]
(Dan seterusnya, untuk semua jenis bawaan)


Definisi jenis memberikan jenis, dan jenis yang mendasari runtuh ke jenis jenis bawaan:

type τ₁ τ₂ τ₂ : κτ₁ : kindOf κ

kindOf kindOf κ kindOf κ
kindOf Map Map
[…]


genre mendefinisikan hubungan subtipe baru:
genre κ = κ₁ | κ₂ κ₁ κ
genre κ = κ₁ | κ₂ κ₂ κ

(Anda dapat mendefinisikan Numeric dan sejenisnya dalam bentuk | .)

genre κ = κ₁ & κ₂ ( κ₃κ₁ ) ( κ₃κ₂ ) κ₃ κ


Aturan ekspansi peti serupa:
type τₙ, … = χ[τ₁, …] ( χ : [κ₁, …] ⇒ [κₙ, …] ) ( τ₁ : κ₁ ) τₙ : κₙ

Ini semua hanya berbicara tentang jenis, tentu saja. Jika Anda ingin mengubahnya menjadi sistem tipe, Anda juga memerlukan aturan tipe. 🙂.


Jadi apa yang Anda gambarkan adalah bentuk parametrik yang cukup dipahami. Itu bagus, karena dipahami dengan baik, tetapi mengecewakan karena tidak membantu menyelesaikan masalah unik yang diperkenalkan Go.

Masalah yang benar-benar menarik dan degil yang diperkenalkan Go terutama seputar pemeriksaan tipe dinamis. Bagaimana seharusnya parameter tipe berinteraksi dengan pernyataan dan refleksi tipe?

(Misalnya, apakah mungkin untuk mendefinisikan antarmuka dengan metode tipe parametrik? Jika demikian, apa yang terjadi jika Anda mengetik-menegaskan nilai antarmuka itu dengan parameter baru saat run-time?)

Pada catatan terkait, apakah ada diskusi tentang cara membuat kode generik di atas tipe bawaan dan tipe yang ditentukan pengguna? Seperti membuat kode yang dapat menangani bigint dan bilangan bulat primitif?

Pada catatan terkait, apakah ada diskusi tentang cara membuat kode generik di atas tipe bawaan dan tipe yang ditentukan pengguna? Seperti membuat kode yang dapat menangani bigint dan bilangan bulat primitif?

Mekanisme berbasis kelas tipe, seperti di Genus dan Familia, dapat melakukan ini secara efisien. Lihat makalah PLDI 2015 kami untuk detailnya.

@DemiMarie
Saya pikir "genre" == "set sifat".

[sunting]
Mungkin traits adalah keywrod yang lebih baik.
Kita dapat melihat setiap jenis juga merupakan kumpulan sifat.

Sebagian besar sifat didefinisikan hanya untuk satu jenis.
Tetapi sifat yang lebih kompleks dapat menentukan hubungan antara dua jenis.

[sunting 2]
asumsikan ada dua himpunan sifat A dan B, kita dapat melakukan operasi berikut:

A + B: union set
A - B: difference set
A & B: intersection set

Kumpulan sifat dari tipe argumen harus berupa kumpulan super dari genre parameter yang sesuai (kumpulan sifat).
Kumpulan sifat dari tipe hasil harus merupakan subkumpulan dari genre hasil yang sesuai (kumpulan sifat).

(MENURUT OPINI SAYA)

Masih saya pikir rebinding Alias ​​​​Tipe adalah cara yang harus dilakukan, untuk menambahkan obat generik ke Go. Itu tidak membutuhkan perubahan besar dalam bahasa. Paket yang digeneralisasi seperti ini, masih bisa digunakan di Go 1.x. Dan tidak perlu menambahkan batasan karena dimungkinkan untuk melakukannya dengan mengatur tipe default untuk tipe alias, ke sesuatu yang sudah memenuhi batasan tersebut. Dan aspek terpenting dari alias tipe rebinding adalah, tipe komposit bawaan (irisan, peta, dan saluran) tidak perlu diubah dan digeneralisasi.

@dc0d

Bagaimana seharusnya mengetik alias menggantikan obat generik?

@sighoya Rebinding Type Alias ​​​​dapat menggantikan obat generik (bukan hanya mengetik alias). Mari kita asumsikan sebuah paket memperkenalkan beberapa alias tipe level paket seperti:

package likedlist

type T = interface{}

type LinkedList struct {
    // ...
}

Jika Type Alias ​​Rebinding (dan fasilitas kompiler) disediakan maka dimungkinkan untuk menggunakan paket ini, untuk membuat daftar tertaut untuk tipe beton yang berbeda, alih-alih antarmuka kosong:

package main

import (
    "likedlist"
)

type intLL = likedlist.LinkedList(likedlist.T = int)
type stringLL = likedlist.LinkedList(likedlist.T = string)

func main() {}

Jika kita menggunakan alias seperti itu, cara berikut lebih bersih.

// pkg.go
package pkg

type ListNode struct {
    prev, next *ListNode
    element    ?Element
}

func Add(x, y ?T) ?T {
    return x+y
}



// main.go
package main

import "pkg"

type intList = pkg.ListNode[Element=int]
func stringAdd = pkg.Add[T=string]

func main() {
}

@dc0d dan bagaimana tepatnya itu akan diterapkan? Kodenya bagus tetapi tidak memberi tahu apa pun tentang cara kerjanya di dalam. Dan, melihat sejarah proposal obat generik, untuk Go itu sangat penting, bukan hanya tampilan dan rasanya.

@dotaheor Itu tidak kompatibel dengan Go 1.x.

@creker Saya telah menerapkan alat (bernama goreuse ) yang menggunakan teknik ini untuk menghasilkan kode dan lahir sebagai konsep untuk Type Alias ​​Rebinding.

Ini dapat ditemukan di sini . Ada video berdurasi 15 menit yang menjelaskan alat tersebut.

@dc0d sehingga berfungsi seperti template C++ yang menghasilkan implementasi khusus. Saya tidak berpikir itu akan diterima sebagai tim Go (dan, sejujurnya, saya dan banyak orang lain di sini) tampaknya menentang apa pun yang mirip dengan templat C++. Ini meningkatkan binari, memperlambat kompilasi, mungkin tidak akan dapat menghasilkan kesalahan yang berarti. Dan, selain itu, tidak kompatibel dengan paket biner saja yang didukung oleh Go. Itulah sebabnya C++ memilih untuk menulis template dalam file header.

@creker

jadi ini berfungsi seperti template C++ yang menghasilkan implementasi khusus untuk setiap jenis yang digunakan.

Saya tidak tahu (Sudah sekitar 16 tahun sejak saya menulis C++). Tapi dari penjelasan Anda sepertinya memang begitu. Namun saya tidak yakin apakah atau bagaimana mereka sama.

Saya tidak berpikir itu akan diterima sebagai tim Go (dan, sejujurnya, saya dan banyak orang lain di sini) tampaknya menentang apa pun yang mirip dengan templat C++.

Tentu semua orang di sini memiliki alasan bagus untuk preferensi mereka berdasarkan prioritas mereka. Pertama dalam daftar saya adalah kompatibilitas dengan Go 1.x.

Ini meningkatkan binari,

Itu mungkin.

memperlambat kompilasi,

Saya sangat meragukan itu (karena dapat dialami dengan goreuse ).

Dan, selain itu, tidak kompatibel dengan paket biner saja yang didukung oleh Go.

Saya tidak yakin. Apakah cara lain untuk mengimplementasikan obat generik mendukung ini?

mungkin tidak akan mampu menghasilkan kesalahan yang berarti.

Ini bisa sedikit merepotkan. Tetap saja itu terjadi pada waktu kompilasi dan dapat dikompensasi, menggunakan beberapa alat, untuk sebagian besar. Selain itu jika alias tipe yang bertindak sebagai parameter tipe untuk paket, adalah sebuah antarmuka, maka dapat dengan mudah diperiksa apakah dapat dialihkan dari tipe yang disediakan beton. Meskipun masalah untuk tipe primitif seperti int dan string dan struct tetap ada.

@dc0d

Saya memikirkannya sedikit.
Selain itu dibuat secara internal pada antarmuka, 'T' dalam contoh Anda

type T=interface{}

diperlakukan sebagai variabel tipe yang dapat diubah, tetapi harus berupa alias untuk tipe tertentu, yaitu referensi const ke suatu tipe.
Yang Anda inginkan adalah Tipe T, tetapi ini menyiratkan pengenalan obat generik.

@sighoya Saya tidak yakin apakah saya mengerti apa yang Anda katakan.

Ini dibuat secara internal pada antarmuka

Tidak benar. Seperti yang dijelaskan dalam komentar asli saya, dimungkinkan untuk menggunakan tipe tertentu yang memenuhi batasan. Misalnya tipe parameter tipe alias dapat dideklarasikan sebagai:

type T = int

Dan hanya tipe yang memiliki + operator (atau - atau * ; tergantung apakah operator tersebut digunakan di badan paket) yang dapat digunakan sebagai nilai tipe yang duduk di parameter tipe itu.

Jadi bukan hanya interface saja yang bisa digunakan sebagai parameter tipe place-holder.

tapi ini akan menyiratkan pengenalan obat generik.

Ini _adalah_ cara untuk memperkenalkan/mengimplementasikan obat generik dalam bahasa Go itu sendiri.

@dc0d

Untuk menyediakan polimorfisme, Anda akan menggunakan antarmuka{} karena ini memungkinkan untuk menyetel T ke tipe apa pun nanti.

Pengaturan 'type T=Int' tidak akan banyak berguna.

Jika Anda akan mengatakan bahwa 'tipe T' tidak dideklarasikan/tidak ditentukan terlebih dahulu yang dapat diatur nanti, maka Anda memiliki sesuatu seperti obat generik.

Masalahnya adalah 'T' menampung lebar modul/paket dan tidak lokal untuk fungsi atau struct apa pun (oke bagus, mungkin deklarasi tipe bersarang dalam struct yang dapat diakses dari luar).

Mengapa tidak menulis saja?:

fun<type T>(t T)

atau

fun[type T](t T)

Selanjutnya kita memerlukan beberapa mesin inferensi tipe untuk menyimpulkan tipe yang tepat saat memanggil fungsi generik atau struct tanpa spesialisasi parameter tipe pada awalnya.

@dc0d menulis

Dan hanya tipe yang memiliki + operator (atau - atau *; tergantung apakah operator tersebut digunakan di badan paket sama sekali) yang dapat digunakan sebagai nilai tipe yang berada di parameter tipe tersebut.

Bisakah Anda menguraikan lebih lanjut tentang ini?

@sighoya

Untuk menyediakan polimorfisme, Anda akan menggunakan antarmuka{} karena ini memungkinkan untuk menyetel T ke tipe apa pun nanti.

Polimorfisme tidak dicapai dengan memiliki tipe yang kompatibel, saat mengikat ulang alias tipe. Satu-satunya kendala sebenarnya adalah, isi paket generik. Mereka harus kompatibel secara mekanis.

Bisakah Anda menguraikan lebih lanjut tentang ini?

Misalnya jika alias tipe parameter tipe level paket didefinisikan seperti:

package genericadd

type T = int

func Add(a, b T) T { return a + b }

Kemudian hampir semua tipe numerik dapat ditetapkan ke T , seperti:

package main

import (
    "genericadd"
)

var add = genericadd.Add(
    T = float64
)

func main() {
    var (
        a, b float64
    )

    println(add(a, b))
}

@dc0d

Namun saya tidak yakin apakah atau bagaimana mereka sama.

Mereka sama dalam arti bahwa mereka bekerja hampir identik dari apa yang saya lihat. Untuk setiap kompilator instantiasi templat kelas akan menghasilkan implementasi unik jika ini adalah pertama kalinya ia melihat penggunaan kombinasi tertentu dari templat kelas dan daftar parameternya. Itu meningkatkan ukuran biner karena Anda sekarang memiliki beberapa implementasi dari templat kelas yang sama. Memperlambat kompilasi karena kompiler sekarang perlu menghasilkan implementasi ini dan melakukan segala macam pemeriksaan. Dalam kasus C++, peningkatan waktu kompilasi bisa sangat besar. Contoh mainan Anda cepat tetapi begitu juga dengan C++.

Saya tidak yakin. Apakah cara lain untuk mengimplementasikan obat generik mendukung ini?

Bahasa lain tidak memiliki masalah dengan itu. Secara khusus, C# sebagai yang paling akrab bagi saya. Tapi itu menggunakan pembuatan kode runtime yang tim Go mengesampingkan sepenuhnya. Java juga berfungsi tetapi implementasinya bukan yang terbaik, untuk sedikitnya. Beberapa proposal ianlancetaylor hanya dapat menangani paket biner dari apa yang saya pahami.

Satu-satunya hal yang saya tidak mengerti adalah apakah paket biner saja harus didukung. Saya tidak melihat mereka disebutkan secara eksplisit dalam proposal. Saya tidak terlalu peduli dengan mereka tetapi tetap saja, ini adalah fitur bahasa.

Hanya untuk menguji pemahaman saya... pertimbangkan repo algoritme salin/tempel ini [ di sini ]. Kecuali jika Anda ingin menggunakan "int", kode tersebut tidak dapat digunakan secara langsung. Itu harus disalin dan ditempel dan dimodifikasi agar berfungsi. Dan dengan modifikasi, maksud saya setiap instance "int" harus diubah ke tipe apa pun yang Anda butuhkan.

Pendekatan tipe alias akan membuat modifikasi sekali, misalnya T, dan menyisipkan baris "tipe T int". Maka kompiler perlu mengikat ulang T ke sesuatu yang lain, katakanlah float64.

Karena itu:
a) Saya berpendapat bahwa tidak akan ada perlambatan kompiler kecuali Anda benar-benar menggunakan teknik ini. Jadi itu adalah pilihan Anda.
b) Mengingat hal-hal vgo baru, di mana beberapa versi dari kode yang sama dapat digunakan ... artinya harus ada beberapa metode untuk menyelipkan sumber yang digunakan agar tidak terlihat, maka pasti kompiler dapat melacak apakah dua penggunaan rebinding yang sama digunakan dan menghindari duplikasi. Jadi saya pikir kode mengasapi akan sama dengan teknik salin/tempel saat ini.

Tampaknya bagi saya bahwa antara alias tipe dan vgo yang akan datang, fondasi untuk pendekatan obat generik ini hampir selesai...

Ada beberapa "tidak diketahui" yang tercantum dalam proposal [ di sini ]. Jadi, alangkah baiknya jika dagingnya sedikit lebih banyak.

@mandolyte Anda dapat menambahkan tingkat tipuan lain dengan membungkus tipe khusus dalam beberapa wadah umum. Dengan begitu implementasi Anda bisa tetap sama. Kompiler kemudian akan melakukan semua keajaiban. Saya pikir proposal parameter tipe Ian berfungsi seperti itu.

Saya pikir pengguna membutuhkan pilihan antara penghapusan tipe dan monomorfisasi.
Yang terakhir adalah mengapa Rust menyediakan abstraksi tanpa biaya. Pergi juga harus.

Pada Senin, 9 April 2018, 8:32 Antonenko [email protected]
menulis:

@mandolyte https://github.com/mandolyte Anda dapat menambahkan level lain
tipuan dengan membungkus jenis khusus dalam beberapa wadah umum. Itu
cara implementasi Anda bisa tetap sama. Kompiler kemudian akan melakukan semua
sihir. Saya pikir proposal parameter tipe Ian berfungsi seperti itu.


Anda menerima ini karena Anda disebutkan.
Balas email ini secara langsung, lihat di GitHub
https://github.com/golang/go/issues/15292#issuecomment-379735199 , atau bisukan
benang
https://github.com/notifications/unsubscribe-auth/AGGWB1v9h5kWmuHCBuoewTTSX751OHgrks5tm1TsgaJpZM4IG-xv
.

Tampak bagi saya bahwa ada kebingungan yang dapat dimengerti dalam diskusi ini tentang tradeoff antara modularitas dan kinerja. Teknik C++ untuk memeriksa ulang dan membuat instance kode generik pada setiap jenis yang digunakannya buruk untuk modularitas, buruk untuk distribusi biner, dan karena kode mengasapi, buruk untuk kinerja. Bagian yang baik dari pendekatan itu adalah ia secara otomatis mengkhususkan kode yang dihasilkan ke tipe yang digunakan, yang sangat membantu ketika tipe yang digunakan adalah tipe primitif seperti int . Java menerjemahkan kode generik secara homogen, tetapi membayar harga dalam kinerjanya, terutama ketika kode tersebut menggunakan tipe T[] .

Untungnya, ada beberapa cara untuk mengatasi ini tanpa non-modularitas C++ dan tanpa pembuatan kode run-time penuh:

  1. Hasilkan instantiasi khusus untuk tipe primitif. Ini dapat dilakukan baik secara otomatis atau dengan arahan programmer. Beberapa pengiriman diperlukan untuk mengakses instantiasi yang benar, tetapi dapat dilipat menjadi pengiriman yang sudah dibutuhkan oleh terjemahan yang homogen. Ini akan bekerja mirip dengan C#, tetapi tidak memerlukan pembuatan kode run-time penuh; sedikit dukungan ekstra mungkin diperlukan dalam waktu proses untuk menyiapkan tabel pengiriman saat kode dimuat.
  2. Gunakan implementasi generik tunggal di mana array T sebenarnya direpresentasikan sebagai array tipe primitif ketika T dipakai sebagai tipe primitif. Pendekatan ini, yang kami gunakan di PolyJ, Genus, dan Familia, sangat meningkatkan kinerja dibandingkan dengan pendekatan Java, meskipun tidak secepat implementasi yang sepenuhnya terspesialisasi.

@dc0d

Polimorfisme tidak dicapai dengan memiliki tipe yang kompatibel, saat mengikat ulang alias tipe. Satu-satunya kendala sebenarnya adalah, isi paket generik. Mereka harus kompatibel secara mekanis.

Ketik alias adalah cara yang salah, karena harus menjadi referensi konstan.
Lebih baik untuk menulis 'T Type' secara langsung dan kemudian Anda melihat Anda memang menggunakan obat generik.

Mengapa Anda ingin menggunakan variabel tipe global 'T' untuk seluruh paket/modul, tipe lokal vars di <> atau [] lebih modular.

@creker

Secara khusus, C# sebagai yang paling akrab bagi saya. Tapi itu menggunakan pembuatan kode runtime yang tim Go mengesampingkan sepenuhnya.

Untuk tipe referensi, tetapi tidak untuk tipe nilai.

@DemiMarie

Saya pikir pengguna membutuhkan pilihan antara penghapusan tipe dan monomorfisasi.
Yang terakhir adalah mengapa Rust menyediakan abstraksi tanpa biaya. Pergi juga harus.

"Jenis Penghapusan" ambigu, saya akan menganggap maksud Anda Penghapusan Parameter Jenis, hal yang disediakan Java yang juga tidak sepenuhnya benar.
Java memiliki monomorfisasi, tetapi monomorfisasi (semi) terus-menerus ke batas atas dalam batasan generik yang sebagian besar adalah Object.
Untuk menyediakan metode dan bidang tipe lain, batas atas dicor secara internal ke tipe Anda yang sesuai yang cukup jelek.
Jika proyek Valhalla akan diterima, hal-hal akan berubah untuk tipe nilai tetapi sayangnya tidak untuk tipe referensi.

Pergi tidak harus melalui Jalan Jawa karena:

"Kompatibilitas biner untuk paket yang dikompilasi tidak dijamin di antara rilis"

sedangkan ini tidak mungkin di Jawa.

Tampak bagi saya bahwa ada kebingungan yang dapat dimengerti dalam diskusi ini tentang tradeoff antara modularitas dan kinerja. Teknik C++ untuk memeriksa ulang dan membuat instance kode generik pada setiap jenis yang digunakannya buruk untuk modularitas, buruk untuk distribusi biner, dan karena kode mengasapi, buruk untuk kinerja.

Jenis pertunjukan apa yang Anda bicarakan di sini?

Jika dengan "kode mengasapi" dan "kinerja" yang Anda maksud adalah "ukuran biner" dan "tekanan cache instruksi", maka masalahnya cukup mudah untuk diselesaikan: selama Anda tidak menyimpan informasi debug secara berlebihan untuk setiap spesialisasi, Anda dapat fungsi runtuh dengan tubuh yang sama ke dalam fungsi yang sama pada waktu link (yang disebut "model Borland" ). Itu secara sepele menangani spesialisasi untuk tipe dan tipe primitif tanpa panggilan ke metode non-sepele.

Jika dengan "kode mengasapi" dan "kinerja" yang Anda maksud adalah "ukuran input tautan" dan "waktu penautan", maka masalahnya juga cukup mudah, jika Anda dapat membuat asumsi tertentu (masuk akal) tentang sistem pembangunan Anda. Alih-alih memancarkan setiap spesialisasi di setiap unit kompilasi, Anda dapat menampilkan daftar spesialisasi yang diperlukan, dan meminta sistem build untuk membuat instance setiap spesialisasi unik tepat satu kali sebelum menautkan ("model Cfront"). IIRC, ini adalah salah satu masalah yang coba diatasi oleh modul C++ .

Jadi, kecuali Anda bermaksud jenis ketiga dari "kode mengasapi" dan "kinerja" yang saya lewatkan, sepertinya Anda sedang berbicara tentang masalah dengan implementasi, bukan spesifikasi: _as selama implementasi tidak terlalu mempertahankan debug informasi,_ masalah kinerja cukup mudah untuk diatasi.


Masalah yang lebih besar untuk Go adalah bahwa, jika kita tidak hati-hati, menjadi mungkin untuk menggunakan pernyataan tipe atau refleksi untuk menghasilkan contoh baru dari tipe parameter pada saat run-time, yang tidak ada jumlah kepintaran implementasi — kekurangan keseluruhan yang mahal -analisis program - dapat memperbaiki.

Itu memang kegagalan modularitas, tetapi ~ tidak ada hubungannya dengan kode mengasapi: sebaliknya, itu berasal dari fakta bahwa jenis fungsi (dan metode) Go tidak menangkap serangkaian kendala yang cukup lengkap pada argumen mereka.

@sighoya

Untuk tipe referensi, tetapi tidak untuk tipe nilai.

Dari apa yang saya baca, C# JIT melakukan spesialisasi saat runtime untuk setiap tipe nilai dan sekali untuk semua tipe referensi. Tidak ada spesialisasi waktu kompilasi (IL-time). Itulah sebabnya pendekatan C# benar-benar diabaikan - Tim Go tidak ingin bergantung pada pembuatan kode runtime karena membatasi platform yang dapat dijalankan Go. Secara khusus, di iOS Anda tidak diizinkan melakukan pembuatan kode saat runtime. Ini berfungsi dan saya benar-benar melakukan beberapa tetapi Apple tidak mengizinkannya di AppStore.

Bagaimana kamu melakukannya?

Pada Senin, 9 April 2018, 15:41 Antonenko [email protected]
menulis:

@sighoya https://github.com/sighoya

Untuk tipe referensi, tetapi tidak untuk tipe nilai.

Dari apa yang saya baca, C# JIT melakukan spesialisasi saat runtime untuk setiap nilai
ketik dan sekali untuk semua jenis referensi. Tidak ada waktu kompilasi
spesialisasi. Itulah sebabnya pendekatan C# benar-benar diabaikan - Go team
tidak ingin bergantung pada pembuatan kode runtime karena membatasi platform Go
dapat berjalan terus. Secara khusus, di iOS Anda tidak diizinkan melakukan pembuatan kode
saat berjalan. Ini berfungsi dan saya benar-benar melakukan beberapa tetapi Apple tidak mengizinkan
itu di AppStore.


Anda menerima ini karena Anda disebutkan.
Balas email ini secara langsung, lihat di GitHub
https://github.com/golang/go/issues/15292#issuecomment-379870005 , atau bisukan
benang
https://github.com/notifications/unsubscribe-auth/AGGWB-tslGeUSGXl2ZlEDLf0dCATUaYvks5tm7lvgaJpZM4IG-xv
.

@DemiMarie meluncurkan kode penelitian lama saya hanya untuk memastikan (penelitian itu dibatalkan karena alasan lain). Sekali lagi, debugger menyesatkan saya. Saya mengalokasikan halaman, menulis beberapa instruksi untuk itu, mprotect dengan PROT_EXEC dan melompat ke sana. Di bawah debugger itu berfungsi. Tanpa aplikasi debugger SIGKILL dengan pesan CODESIGN di log kerusakan, seperti yang diharapkan. Jadi, itu tidak berfungsi bahkan tanpa AppStore. Argumen yang lebih kuat terhadap pembuatan kode runtime jika iOS penting untuk Go.

Pertama, akan sangat membantu untuk merenungkan 5 Aturan Pemrograman Rob Pike sekali lagi.

Kedua (IMHO):

Tentang kompilasi lambat dan ukuran biner, berapa banyak tipe generik yang digunakan dalam tipe umum aplikasi yang sedang dikembangkan menggunakan Go (_n biasanya kecil_ dari Aturan 3)? Kecuali jika masalahnya membutuhkan tingkat kardinalitas yang tinggi dalam konsep konkret (jumlah jenis yang tinggi) overhead dapat diabaikan. Bahkan kemudian saya akan berpendapat bahwa ada sesuatu yang salah dengan pendekatan itu. Saat menerapkan sistem e-niaga, tidak ada yang mendefinisikan jenis terpisah untuk setiap jenis produk dan variasinya serta kemungkinan penyesuaian.

Verbositas adalah bentuk kesederhanaan dan keakraban yang baik (misalnya dalam sintaksis) yang membuat segalanya lebih jelas dan bersih. Sementara saya ragu bahwa kode mengasapi akan lebih tinggi menggunakan Type Alias ​​Rebinding, saya suka sintaks Go-ish yang familiar dan verbositas yang jelas menyertainya. Salah satu tujuan Go adalah mudah dibaca (sementara saya pribadi merasa relatif mudah dan menyenangkan untuk menulis juga).

Saya tidak mengerti bagaimana hal itu dapat merusak kinerja karena pada saat runtime, hanya tipe terikat beton yang digunakan yang telah dihasilkan pada waktu kompilasi. Tidak ada overhead runtime.

Satu-satunya perhatian dengan Type Alias ​​Rebinding yang saya lihat, mungkin distribusi biner.

Kerusakan kinerja @dc0d biasanya berarti mengisi cache instruksi karena implementasi templat kelas yang berbeda. Bagaimana tepatnya kaitannya dengan kinerja nyata adalah pertanyaan terbuka, saya tidak tahu tolok ukur apa pun, tetapi secara teoritis itu adalah masalah.

Adapun ukuran biner. Ini adalah masalah teoretis lain yang biasanya diangkat orang (seperti yang saya lakukan sebelumnya) tetapi bagaimana kode sebenarnya akan menderita, sekali lagi, merupakan pertanyaan terbuka. Misalnya, spesialisasi untuk semua jenis pointer dan antarmuka mungkin sama, saya pikir. Tetapi spesialisasi untuk semua jenis nilai akan menjadi unik. Dan itu juga termasuk struct. Menggunakan wadah generik untuk menyimpannya adalah hal biasa dan akan menyebabkan kode yang membengkak karena implementasi wadah generik tidak kecil.

Satu-satunya perhatian dengan Type Alias ​​Rebinding yang saya lihat, mungkin distribusi biner.

Di sini saya masih tidak yakin. Apakah proposal generik harus mendukung paket biner saja atau kita bisa menyebutkan bahwa paket biner saja tidak mendukung generik. Itu akan jauh lebih mudah, itu pasti.

Seperti yang telah disebutkan sebelumnya, jika seseorang tidak perlu mendukung debugging, satu
dapat menggabungkan instantiasi template yang identik.

Pada Selasa, 10 Apr 2018, 5:46 Kaveh Shahbazian [email protected]
menulis:

Pertama, akan sangat membantu untuk merenungkan 5 Aturan Pemrograman Rob Pike
https://users.ece.utexas.edu/%7Eadnan/pike.html sekali lagi.

Kedua (IMHO):

Tentang kompilasi lambat dan ukuran biner, berapa banyak tipe generik yang digunakan dalam
jenis aplikasi umum yang sedang dikembangkan menggunakan Go ( n isbiasanya kecil dari Aturan 3)? Kecuali jika masalahnya membutuhkan tingkat yang tinggi
kardinalitas dalam konsep konkret (jumlah jenis tinggi) yang dapat di atas kepala
diabaikan. Bahkan kemudian saya akan berpendapat bahwa ada sesuatu yang salah dengan itu
mendekati. Saat menerapkan sistem e-niaga, tidak ada yang mendefinisikan secara terpisah
jenis untuk setiap jenis produk dan variasinya dan kemungkinannya
kustomisasi.

Verbositas adalah bentuk kesederhanaan dan keakraban yang baik (misalnya dalam
sintaks) yang membuat segalanya lebih jelas dan bersih. Sementara aku meragukan itu
kode mengasapi akan lebih tinggi menggunakan Type Alias ​​Rebinding, saya suka
sintaks Go-ish yang familier dan verbositas yang jelas menyertainya. Satu dari
tujuan Go adalah mudah dibaca (sementara saya pribadi menemukannya
relatif mudah dan menyenangkan untuk menulis juga).

Saya tidak mengerti bagaimana itu dapat merusak kinerja karena hanya saat runtime
jenis terikat beton sedang digunakan yang telah dihasilkan di
waktu kompilasi. Tidak ada overhead runtime.

Satu-satunya masalah dengan Type Alias ​​Rebinding yang saya lihat, mungkin biner
distribusi.


Anda menerima ini karena Anda disebutkan.
Balas email ini secara langsung, lihat di GitHub
https://github.com/golang/go/issues/15292#issuecomment-380040032 , atau bisukan
benang
https://github.com/notifications/unsubscribe-auth/AGGWB6aDfoHz2wbsmu8mCGEt652G_VE9ks5tnH9xgaJpZM4IG-xv
.

Instansiasi bahkan tidak perlu "identik" dalam arti "menggunakan argumen yang sama", atau bahkan "menggunakan argumen dengan tipe dasar yang sama". Mereka hanya perlu cukup dekat untuk menghasilkan kode yang sama. (Untuk Go, itu juga menyiratkan "topeng penunjuk yang sama".)

@creker

Dari apa yang saya baca, C# JIT melakukan spesialisasi saat runtime untuk setiap tipe nilai dan sekali untuk semua tipe referensi. Tidak ada spesialisasi waktu kompilasi (IL-time).

Nah, ini terkadang sedikit rumit karena kode byte mereka ditafsirkan tepat pada waktunya sebelum kode dieksekusi, jadi pembuatan kode dilakukan sebelum menjalankan program tetapi setelah kompilasi, jadi Anda benar dalam arti vm yang sedang berjalan saat kode dihasilkan.

Saya pikir sistem generik c# akan baik-baik saja jika kita menghasilkan kode pada waktu kompilasi.
Pembuatan kode runtime dalam arti c# tidak dimungkinkan dengan go, karena go bukan vm.

@dc0d

Satu-satunya perhatian dengan Type Alias ​​Rebinding yang saya lihat, mungkin distribusi biner.

Bisakah Anda menguraikan sedikit.

@sighoya Kesalahan saya; Maksud saya bukan distribusi biner tetapi paket biner - yang secara pribadi saya tidak tahu betapa pentingnya itu.

@creker Kesimpulan yang bagus! (MO) Kecuali alasan kuat ditemukan, segala bentuk overloading konstruksi bahasa Go harus dihindari. Salah satu alasan untuk menggunakan Type Alias ​​Rebinding adalah untuk menghindari kelebihan beban tipe komposit bawaan seperti irisan atau peta.

Verbositas adalah bentuk kesederhanaan dan keakraban yang baik (misalnya dalam sintaksis) yang membuat segalanya lebih jelas dan bersih. Sementara saya ragu bahwa kode mengasapi akan lebih tinggi menggunakan Type Alias ​​Rebinding, saya suka sintaks Go-ish yang familiar dan verbositas yang jelas menyertainya. Salah satu tujuan Go adalah mudah dibaca (sementara saya pribadi merasa relatif mudah dan menyenangkan untuk menulis juga).

Saya tidak setuju dengan anggapan ini. Proposal Anda akan memaksa pengguna untuk melakukan hal tersulit yang diketahui oleh programmer mana pun - memberi nama. Jadi kita akan berakhir dengan kode yang penuh dengan notasi hungaria, yang tidak hanya terlihat buruk, tetapi juga bertele-tele dan menyebabkan gagap. Selain itu, proposal lain juga membawa sintaks go-ish, dan pada saat yang sama tidak memiliki masalah ini.

Ada tiga kategori nama yang harus kita pikirkan setiap hari:

  • Untuk Entitas/Logika Domain
  • Jenis Data/Logika Alur Kerja Program
  • Layanan/Jenis Data/Logika Antarmuka

Berapa kali seorang programmer berhasil menghindari penamaan apa pun dalam kodenya?

Sulit atau tidak itu perlu dilakukan setiap hari. Dan sebagian besar rintangannya datang dari ketidakmampuan dalam menyusun basis kode - bukan kesulitan dari proses penamaan itu sendiri. Kutipan itu - setidaknya dalam bentuknya saat ini - telah merugikan dunia pemrograman sejauh ini. Ini hanya mencoba untuk menekankan pentingnya penamaan. Karena kita berkomunikasi melalui nama dalam kode kita.

Dan nama menjadi jauh lebih kuat ketika mereka menyertai praktik penataan kode; baik dalam hal tata letak kode (file, struktur direktori, paket/modul) dan praktik (pola desain, abstraksi layanan - seperti REST, manajemen sumber daya - pemrograman bersamaan, mengakses hard drive, throughput/latency).

Adapun sintaks dan verbositas, saya lebih menyukai verbositas daripada keringkasan yang cerdas (setidaknya dalam konteks Go) - sekali lagi, Go dimaksudkan agar mudah dibaca, belum tentu mudah untuk ditulis (yang anehnya menurut saya juga bagus dalam hal itu) .

Saya membaca banyak laporan pengalaman dan proposal tentang mengapa dan bagaimana menerapkan obat generik di Go.

Apakah Anda keberatan jika saya mencoba untuk benar-benar menerapkannya di gomacro juru bahasa Go saya?

Saya memiliki beberapa pengalaman tentang topik ini, setelah menambahkan obat generik ke dua bahasa di masa lalu

  1. bahasa yang sekarang ditinggalkan yang saya buat kembali ketika saya masih naif :) Itu berubah menjadi kode sumber C
  2. Common Lisp dengan perpustakaan saya cl-parametric-types - ini juga mendukung spesialisasi sebagian dan penuh dari tipe dan fungsi generik

@ cosmos72 itu akan membuat laporan pengalaman yang bagus untuk melihat prototipe teknik yang menjaga keamanan tipe.

Baru mulai mengerjakannya. Anda dapat mengikuti perkembangannya di https://github.com/cosmos72/gomacro/tree/generics-v1

Saat ini saya mulai dengan campuran (sedikit dimodifikasi) dari proposal Ian ketiga dan keempat yang tercantum di https://github.com/golang/proposal/blob/master/design/15292-generics.md#Proposal

@cosmos72 Ada ringkasan proposal di tautan di bawah ini. Apakah paduan Anda salah satunya?
https://docs.google.com/document/d/1vrAy9gMpMoS3uaVphB32uVXX4pi-HnNjkMEgyAHX4N4

Saya telah membaca dokumen itu, itu merangkum banyak pendekatan berbeda untuk obat generik dengan berbagai bahasa pemrograman.

Saat ini saya akan menuju teknik "Spesialisasi tipe" yang digunakan oleh C++, Rust, dan lainnya, mungkin dengan sedikit "Cakupan templat berparameter" karena Go sintaks paling umum untuk tipe baru adalah type ( Foo ...; Bar ...) dan saya memperluas ke template[T1,T2...] type ( Foo ...; Bar ...) .
Juga, saya menjaga pintu tetap terbuka untuk "Spesialisasi terbatas".

Saya juga ingin mengimplementasikan "Spesialisasi fungsi polimorfik", yaitu untuk mengatur agar spesialisasi secara otomatis disimpulkan oleh bahasa di situs panggilan jika tidak ditentukan oleh programmer, tetapi saya kira itu mungkin agak rumit untuk diterapkan. Kita lihat.

Campuran yang saya maksud adalah antara https://github.com/golang/proposal/blob/master/design/15292/2013-10-gen.md dan https://github.com/golang/proposal/blob/ master/design/15292/2013-12-type-params.md

Pembaruan: untuk menghindari spamming masalah Go resmi ini di luar pengumuman awal, mungkin lebih baik untuk melanjutkan diskusi khusus gomacro di gomacro edisi #24: tambahkan obat generik

Pembaruan 2: fungsi templat pertama berhasil dikompilasi dan dieksekusi. Lihat https://github.com/cosmos72/gomacro/tree/generics-v1

Sebagai catatan, dimungkinkan untuk mengulangi pendapat saya (tentang obat generik dan Type Alias ​​Rebinding):

Generik harus ditambahkan sebagai fitur kompiler (pembuatan kode, templat, dll), bukan fitur bahasa (campur tangan dengan sistem tipe Go di semua tingkatan).

@dc0d
Tetapi apakah templat C++ bukan fitur kompiler dan bahasa?

@sighoya Terakhir kali saya menulis C++ secara profesional adalah sekitar tahun 2001. Jadi saya mungkin salah. Tetapi dengan asumsi implikasi penamaan itu akurat - bagian "templat" - ya (atau lebih tepatnya tidak); itu mungkin fitur kompiler (dan bukan fitur bahasa), disertai dengan beberapa konstruksi bahasa, yang kemungkinan besar tidak membebani konstruksi bahasa apa pun yang terlibat dalam sistem tipe.

Saya mendukung @dc0d. Jika Anda mempertimbangkannya, fitur ini tidak lebih dari generator kode terintegrasi.

Ya: ukuran biner mungkin dan AKAN meningkat, tetapi saat ini kami menggunakan pembuat kode, yang hampir sama tetapi sebagai fitur eksternal. Jika saya harus membuat template saya sebagai:

type BinaryTreeOfStrings struct {
    left, right *BinaryTreeOfStrings;
    string content;
}

// Its methods here

type BinaryTreeOfBigInts struct {
    left, right *BinaryTreeOfBigInts;
    uint64 content;
}

// AGAIN the same methods but different type

... Saya sangat menyukai itu, daripada menyalin atau menggunakan alat eksternal, fitur ini menjadi bagian dari kompilator itu sendiri.

Tolong dicatat:

  • Ya, kode akhir akan diduplikasi. Benar seperti kita menggunakan genset. Dan biner akan lebih besar.
  • Ya, idenya tidak orisinal, tetapi dipinjam dari C++.
  • Ya, fungsi MyTypetidak melibatkan apa pun dengan tipe T (langsung atau tidak langsung) juga akan diulang. Itu dapat dioptimalkan (mis. metode yang merujuk sesuatu dengan tipe T -selain penunjuk ke objek penerima pesan- akan dihasilkan untuk setiap T ; metode yang menyimpan pemanggilan ke metode yang akan dihasilkan untuk setiap T , juga akan dihasilkan untuk setiap T , secara rekursif - sementara metode di mana satu-satunya referensi mereka ke T adalah *T di penerima, dan metode lain yang hanya memanggil metode aman tersebut dan memenuhi kriteria yang sama, hanya dapat dibuat satu kali). Bagaimanapun, IMO poin ini besar dan kurang tepat: Saya akan cukup senang bahkan jika pengoptimalan ini tidak ada.
  • Jenis argumen harus eksplisit menurut saya. Khususnya ketika suatu objek memenuhi antarmuka yang berpotensi tak terbatas. Sekali lagi: pembuat kode.

Sejauh ini dalam komentar saya, proposal saya adalah mengimplementasikannya apa adanya: sebagai pembuat kode yang didukung kompiler , alih-alih alat eksternal.

Akan sangat disayangkan jika Go mengikuti rute C++. Banyak orang melihat pendekatan C++ sebagai kekacauan yang telah membuat programmer menentang seluruh ide generik: kesulitan debugging, kurangnya modularitas, kode mengasapi. Semua solusi "pembuat kode" sebenarnya hanyalah substitusi makro — jika itu cara Anda ingin menulis kode, mengapa kami bahkan memerlukan dukungan kompiler?

@andrewcmyers Saya punya proposal ini Ketik Alias ​​​​Rebinding di mana kami hanya menulis paket normal dan alih-alih menggunakan interface{} secara eksplisit, kami hanya menggunakannya sebagai type T = interface{} sebagai parameter generik tingkat paket. Dan itu saja.

  • Kami men-debugnya seperti paket normal - ini adalah kode aktual, bukan makhluk paruh waktu perantara.
  • Tidak perlu campur tangan dengan sistem tipe Go di semua tingkatan - pikirkan tentang penugasan saja.
  • Hal ini eksplisit. Tidak ada mojo tersembunyi. Tentu saja orang mungkin merasa tidak dapat menyambungkan panggilan umum dengan mulus, sebuah kelemahan. Saya melihatnya sebagai draw-forward! Mengubah jenis dalam dua panggilan berturut-turut, dalam satu pernyataan tidak Goish (IMO).
  • Dan yang terbaik dari semuanya adalah kompatibel dengan seri Go 1.x (x >= 8).

Meskipun idenya bukanlah hal baru, cara yang memungkinkan Go untuk mengimplementasikannya adalah pragmatis dan jelas.

Bonus lebih lanjut: tidak ada operator yang kelebihan beban di Go. Tetapi dengan mendefinisikan nilai default dari alias tipe sebagai (misalnya) type T = int , bukan satu-satunya tipe valid yang dapat digunakan untuk menyesuaikan paket generik ini, adalah tipe numerik yang memiliki implementasi internal untuk + operator.

Juga parameter tipe alias dapat dipaksa untuk memenuhi lebih dari satu antarmuka hanya dengan menambahkan beberapa tipe dan pernyataan validator.

Sekarang, itu akan sangat buruk menggunakan notasi eksplisit apa pun untuk tipe generik yang memiliki parameter yang mengimplementasikan antarmuka Error dan Stringer dan juga merupakan tipe numerik yang mendukung operator + !

sekarang kami menggunakan pembuat kode, yang hampir sama tetapi sebagai fitur eksternal.

Perbedaannya adalah, bahwa cara yang diterima secara luas untuk melakukan pembuatan kode (melalui go generate ) terjadi pada waktu komit/pengembangan, bukan pada waktu kompilasi. Melakukannya pada waktu kompilasi menyiratkan bahwa Anda perlu mengizinkan eksekusi kode arbitrer di kompiler, perpustakaan dapat meledakkan waktu kompilasi berdasarkan urutan besarnya dan/atau Anda akan memiliki dependensi build yang terpisah (yaitu kode tidak lagi dapat dibangun hanya dengan Go alat). Saya suka Go karena mendorong permintaan pemrograman meta ke pengembang hulu.

Artinya, karena semua pendekatan untuk memecahkan masalah ini, pendekatan ini juga memiliki kelemahan dan melibatkan pengorbanan. Secara pribadi, saya berpendapat bahwa obat generik yang sebenarnya dengan dukungan dalam sistem tipe tidak hanya lebih baik (yaitu memiliki kumpulan fitur yang lebih kuat) tetapi juga dapat mempertahankan keunggulan kompilasi yang dapat diprediksi dan aman.

Saya akan membaca semua hal di atas, saya berjanji, namun saya akan menambahkan sedikit juga - GoLang SDK untuk Apache Beam tampaknya contoh yang cukup terang/pameran masalah yang harus ditanggung oleh perancang perpustakaan untuk melakukan apa pun _dengan benar_ tingkat tinggi.

Setidaknya ada dua implementasi eksperimental untuk generik Go. Awal minggu ini saya menghabiskan waktu dengan (1). Saya senang mengetahui bahwa dampak keterbacaan kode sangat minim. Dan saya menemukan penggunaan fungsi anonim untuk memberikan tes kesetaraan bekerja dengan baik; jadi saya yakin bahwa operator overloading tidak diperlukan. Satu-satunya masalah yang saya temukan adalah dalam penanganan kesalahan. Ungkapan umum "return nil,err" tidak akan berfungsi jika tipenya, katakanlah, integer atau string. Ada beberapa cara untuk menyiasatinya, semuanya dengan biaya yang rumit. Saya mungkin agak aneh, tapi saya suka penanganan kesalahan Go. Jadi ini mengarahkan saya untuk mengamati bahwa solusi generik Go harus memiliki kata kunci universal untuk nilai nol suatu tipe. Kompiler hanya akan menggantinya dengan nol untuk tipe numerik, string kosong untuk tipe string, dan nil untuk struct.

Meskipun implementasi ini tidak menerapkan pendekatan tingkat paket, tentu wajar untuk melakukannya. Dan, tentu saja, implementasi ini tidak membahas semua detail teknis tentang ke mana kode yang dibuat oleh kompiler harus pergi (jika di mana saja), bagaimana debugger kode akan bekerja, dll.

Cukup menyenangkan menggunakan kode algoritme yang sama untuk bilangan bulat dan sesuatu seperti Point:

type Point struct {
    x,y int
}

Lihat (2) untuk pengujian dan pengamatan saya.

(1) https://github.com/albrow/fo; yang lainnya adalah https://github.com/cosmos72/gomacro#generics yang disebutkan di atas
(2) https://github.com/mandolyte/fo-experiments

@mandolyte Anda dapat menggunakan *new(T) untuk mendapatkan nilai nol jenis apa pun.

Konstruksi bahasa seperti default(T) atau zero(T) (yang pertama adalah yang pertama
di C# IIRC) akan jelas, tetapi OTOH lebih panjang dari *new(T) (walaupun lebih
performans).

06-07-2018 9:15 GMT-05:00 Tom Thorogood [email protected] :

@mandolyte https://github.com/mandolyte Anda dapat menggunakan *new(T) untuk mendapatkan
nilai nol jenis apa pun.


Anda menerima ini karena Anda berkomentar.
Balas email ini secara langsung, lihat di GitHub
https://github.com/golang/go/issues/15292#issuecomment-403046735 , atau bisukan
benang
https://github.com/notifications/unsubscribe-auth/AlhWhQ5cQwnc3x_XUldyJXCHYzmr6aN3ks5uD3ETgaJpZM4IG-xv
.

--
Ini adalah tes untuk tanda tangan email yang akan digunakan di TripleMint

19642 adalah untuk membahas nilai nol generik

@tmthrgd Entah bagaimana saya melewatkan berita gembira kecil itu. Terima kasih!

pendahuluan

Generik adalah tentang mengkhususkan konstruksi yang dapat disesuaikan. Tiga kategori spesialisasi adalah:

  • Jenis Spesialisasi, Type<T> - sebuah _array_;
  • Mengkhususkan Diri pada Komputasi, F<T>(T) atau F<T>(Type<T>) - _array yang dapat diurutkan_;
  • Notasi Khusus, _LINQ_ misalnya - pernyataan select atau for di Go;

Tentu saja ada bahasa pemrograman yang menyajikan konstruksi yang lebih umum. Tetapi bahasa pemrograman konvensional seperti _C++_, _C#_ atau _Java_ menyediakan konstruksi bahasa yang kurang lebih terbatas pada daftar ini.

pikiran

Kategori pertama dari tipe/konstruksi generik harus tipe agnostik.

Kategori kedua dari tipe/konstruksi generik perlu _bertindak_ pada _properti_ dari parameter tipe. Misalnya _array yang dapat diurutkan_ harus dapat _membandingkan_ _properti yang dapat dibandingkan_ dari item-itemnya. Dengan asumsi T.(P) adalah properti dari T dan A(T.(P)) adalah perhitungan/tindakan yang bertindak atas properti itu, (A, .(P)) dapat diterapkan baik untuk setiap item individual atau dinyatakan sebagai komputasi khusus, diteruskan ke komputasi asli yang dapat disesuaikan. Contoh dari kasus terakhir di Go adalah antarmuka sort.Interface yang juga memiliki fungsi pasangan terpisah sort.Reverse .

Kategori ketiga dari tipe/konstruk generik adalah notasi bahasa _type-specialized_ - tampaknya bukan Go thing _in general_.

pertanyaan

bersambung ...

Umpan balik apa pun yang lebih deskriptif daripada emoji sangat disambut!

@dc0d Saya akan merekomendasikan mempelajari "Elemen Pemrograman" Sepanov sebelum mencoba mendefinisikan Generik. TL;DR adalah kita menulis kode konkret untuk memulai, katakanlah sebuah algoritma yang mengurutkan array. Kemudian kami menambahkan tipe koleksi lain seperti Btree, dll. Kami melihat kami menulis banyak salinan dari algoritma pengurutan yang pada dasarnya sama, jadi kami mendefinisikan beberapa konsep, katakanlah 'dapat diurutkan'. Sekarang kami ingin mengkategorikan algoritma pengurutan, mungkin dengan pola akses yang mereka butuhkan, katakan saja maju, satu pass (aliran), hanya teruskan beberapa pass (daftar tertaut tunggal), dua arah (daftar tertaut ganda), akses acak (daftar tertaut tunggal). Himpunan). Saat menambahkan tipe koleksi baru, kita hanya perlu menunjukkan kategori "koordinat" mana yang termasuk dalam kategori tersebut untuk mendapatkan akses ke semua algoritme pengurutan yang relevan. Kategori algoritme ini sangat mirip dengan antarmuka 'Go'. Saya akan mencari untuk memperluas antarmuka di Go untuk mendukung beberapa parameter tipe, dan tipe abstrak/terkait. Saya tidak berpikir fungsi memerlukan parameterisasi tipe ad-hoc.

@dc0d Sebagai upaya untuk memecah obat generik menjadi bagian-bagian komponen, saya tidak menganggap 3, "notasi khusus," sebagai bagiannya yang terpisah sebelumnya. Mungkin itu bisa dicirikan sebagai mendefinisikan DSL dengan memanfaatkan batasan tipe.

Saya mungkin berpendapat bahwa 1 dan 2 Anda adalah "struktur data" dan "algoritma", masing-masing. Dengan terminologi itu, sedikit lebih jelas mengapa mungkin sulit untuk memisahkan mereka secara bersih, karena mereka seringkali sangat bergantung satu sama lain. Tapi sort.Interface adalah contoh yang cukup bagus di mana Anda dapat menarik garis antara penyimpanan dan perilaku (dengan sedikit gula baru-baru ini untuk membuatnya lebih baik), karena mengkodekan persyaratan yang Dapat Diindeks dan Dapat Dibandingkan ke dalam perilaku minimum yang diperlukan untuk mengimplementasikan algoritme pengurutan dengan "swap" dan "kurang" (dan len). Tapi ini tampaknya memecah struktur data yang lebih rumit seperti pohon atau tumpukan, yang keduanya saat ini membutuhkan beberapa perubahan untuk dipetakan ke dalam perilaku murni sebagai antarmuka Go.

Saya bisa membayangkan tambahan generik yang relatif kecil untuk antarmuka (atau sebaliknya) yang dapat memungkinkan sebagian besar struktur data buku teks dan algoritme diimplementasikan relatif bersih tanpa liuk (seperti sort.Interface saat ini), tetapi tidak cukup kuat untuk merancang DSL. Apakah kita ingin membatasi diri kita pada implementasi obat generik yang dibatasi ketika kita akan kesulitan menambahkan obat generik sama sekali adalah pertanyaan yang berbeda.

Struktur koordinat @infogulch untuk pohon biner adalah "koordinat bifurkasi", dan ada persamaan untuk pohon lain. Namun Anda juga dapat memproyeksikan pemesanan pohon melalui salah satu dari tiga pesanan, pre-order, in-order, dan post-order. Setelah memutuskan salah satunya, pohon dapat dialamatkan sebagai koordinat dua arah, dan keluarga algoritma pengurutan yang didefinisikan pada koordinat dua arah akan menjadi efisien secara optimal.

Intinya adalah Anda mengkategorikan algoritma pengurutan berdasarkan pola aksesnya. Hanya ada sejumlah algoritma pengurutan yang optimal untuk setiap pola akses. Anda tidak peduli tentang struktur data pada saat ini. Berbicara tentang struktur yang lebih kompleks tidak tepat, kami ingin mengkategorikan keluarga algoritma pengurutan bukan struktur data. Data apa pun yang Anda miliki, Anda harus menggunakan salah satu algoritme yang ada untuk mengurutkannya, jadi pertanyaannya adalah kategorisasi pola-akses data yang mana dari algoritme pengurutan yang optimal untuk struktur data yang Anda miliki.

(MENURUT OPINI SAYA)

@infogulch

Mungkin itu bisa dicirikan sebagai mendefinisikan DSL dengan memanfaatkan batasan tipe

Kamu benar. Tetapi karena mereka adalah bagian dari rangkaian konstruksi bahasa, IMO menyebutnya DSL akan sedikit tidak akurat.

1 dan 2 ... seringkali sangat tergantung

Sekali lagi benar. Tetapi ada banyak kasus yang memerlukan jenis wadah untuk diedarkan, sementara penggunaan sebenarnya belum diputuskan - pada saat itu dalam sebuah program. Itu sebabnya 1 perlu dipelajari sendiri.

sort.Interface adalah contoh yang cukup bagus di mana Anda dapat menarik garis antara _storage_ dan _behavior_

Baik mengatakan;

ini tampaknya memecah struktur data yang lebih rumit

Itu adalah salah satu pertanyaan saya: untuk menggeneralisasi parameter tipe dan menggambarkannya dalam batasan (seperti List<T> where T:new, IDisposable ) atau untuk memberikan _protokol_ umum yang berlaku untuk semua item (dari satu set; dari tipe tertentu)?

@keean

pertanyaannya menjadi yang mana dari kategorisasi pola akses data yang tersedia dari algoritma pengurutan yang optimal untuk struktur data yang Anda miliki

Benar. Mengakses dengan indeks adalah _properti_ dari irisan (atau larik). Jadi persyaratan pertama untuk wadah yang dapat diurutkan (atau wadah yang dapat diurutkan _tree_ - apa pun algoritme _tree_nya) adalah menyediakan utilitas _access & mutate (swap)_. Persyaratan kedua adalah item harus sebanding. Itulah bagian yang membingungkan (bagi saya) tentang apa yang Anda sebut algoritme: persyaratan harus dipenuhi di kedua sisi (pada wadah dan pada parameter tipe). Itulah intinya saya tidak bisa membayangkan implementasi generik yang pragmatis di Go. Setiap sisi masalah dapat digambarkan dalam istilah antarmuka dengan sempurna. Tetapi bagaimana menggabungkan keduanya dalam notasi yang efektif?

Algoritma @dc0d membutuhkan antarmuka, struktur data menyediakannya. Ini cukup untuk generalisasi penuh, asalkan antarmukanya cukup kuat. Antarmuka diparameterisasi berdasarkan tipe, tetapi Anda memerlukan variabel tipe.

Mengambil contoh 'sort', 'Ord' adalah properti dari tipe yang disimpan dalam wadah, bukan wadah itu sendiri. Pola akses adalah properti dari wadah. Pola akses sederhana adalah 'iterator', tetapi nama itu berasal dari C++, Stepanov lebih memilih 'koordinat' karena dapat diterapkan ke wadah multi-dimensi yang lebih kompleks.

Mencoba mendefinisikan sort, kami menginginkan sesuatu seperti ini:

bubble_sort : forall T U I => T U -> T U requires
   ForwardIterator<T>, Readable<T>, Writable<T>,
   Ord<U>,  ValueType(T) == U, Distance type(T) == I

Catatan: Saya tidak menyarankan notasi ini, hanya mencoba menarik beberapa pekerjaan terkait lainnya, klausa require ada dalam sintaks yang disukai oleh Stepanov, tipe fungsinya berasal dari Haskell, yang kelas tipenya mungkin mewakili implementasi yang baik dari konsep ini.

@keean
Mungkin saya salah paham dengan Anda, tetapi saya rasa Anda tidak dapat membatasi algoritme hanya untuk antarmuka saja, setidaknya dalam cara antarmuka didefinisikan sekarang.
Pertimbangkan sort.Slice misalnya, kami tertarik untuk menyortir irisan, dan saya tidak melihat bagaimana seseorang akan membangun antarmuka yang akan mewakili semua irisan.

@urandom Anda mengabstraksikan algoritme, bukan koleksi. Jadi Anda bertanya, pola akses data apa yang ada dalam algoritme "sortir", dan kemudian mengklasifikasikannya. Jadi tidak masalah apakah wadahnya adalah "slice" kami tidak mencoba untuk mendefinisikan semua operasi yang mungkin ingin Anda lakukan pada sebuah slice, kami mencoba untuk menentukan persyaratan dari suatu algoritma dan menggunakannya untuk mendefinisikan sebuah antarmuka. Sebuah irisan tidak istimewa, itu hanya tipe T yang dapat kita definisikan satu set operasi.

Jadi antarmuka berhubungan dengan pustaka algoritme, dan Anda dapat menentukan antarmuka Anda sendiri untuk struktur data Anda sendiri agar dapat menggunakan algoritme tersebut. Pustaka bisa datang dengan antarmuka yang telah ditentukan sebelumnya untuk tipe bawaan.

@keean
Saya pikir itu yang Anda maksud. Tetapi dalam konteks Go, itu mungkin berarti bahwa perlu ada perombakan signifikan dari apa yang dapat didefinisikan oleh antarmuka. Saya membayangkan bahwa berbagai operasi bawaan, seperti iterasi, atau operator, perlu diekspos melalui metode agar hal-hal seperti sort.Slice atau math.Max dibuat generik melalui antarmuka.

Jadi, Anda harus memiliki dukungan untuk antarmuka berikut (kode semu):

type [T] OrderedIterator interface {
   Len() int
   ValueAt(i int) *T
}

...
package sort

func [T] Slice(s [T]OrderedIterator, func(i, j int) bool) {
   ...
}

dan semua irisan akan memiliki metode ini?

@urandom Iterator bukan abstraksi koleksi, tetapi abstraksi referensi/penunjuk ke dalam koleksi. Misalnya forward iterator dapat memiliki satu metode 'penerus' (terkadang 'berikutnya'). Mampu mengakses data di lokasi iterator bukanlah properti iterator (jika tidak, Anda akan berakhir dengan rasa iterator baca/tulis/bisa berubah). Yang terbaik adalah mendefinisikan "referensi" secara terpisah sebagai antarmuka yang Dapat Dibaca, Dapat Ditulis, dan Dapat Diubah:

type T ForwardIterator interface {
   type DistanceType D
   successor(x T) T
}

type T Readable interface {
   type ValueType U 
   source(x T) U
}

Catatan: Jenis 'T' bukanlah irisan, tetapi jenis iterator pada irisan. Ini bisa menjadi penunjuk biasa, jika kita mengadopsi gaya C++ untuk meneruskan iterator awal dan akhir ke fungsi seperti sort.

Untuk iterator akses acak, kita akan berakhir dengan sesuatu seperti:

type T RandomIterator interface {
   type DistanceType D
   setPosition(x DistanceType)
}

Jadi iterator/koordinat adalah abstraksi dari referensi ke koleksi, bukan koleksi itu sendiri. Nama 'koordinat' mengungkapkan ini dengan cukup baik, jika Anda menganggap iterator sebagai koordinat, dan koleksi sebagai peta.

Bukankah kami menjual Go short dengan tidak memanfaatkan penutupan fungsi dan fungsi anonim? Memiliki fungsi/metode sebagai tipe kelas satu di Go dapat membantu. Misalnya, menggunakan sintaks dari albrow/fo , pengurutan gelembung mungkin terlihat seperti ini:

type SortableContainer[C,T] struct {
    Less func(C,T,T) bool
    Swap func(C,int,int)
    Next func(C) (T,bool)
}

func (bs *SortableContainer[C,T]) BubbleSort(container C, e1,e2 T) {    
    swapCount := 1
    var item1, item2 T
    item1, ok1 = bs.Next()
    if !ok1 {return}
    item2, ok2 = bs.Next()
    if !ok2 {return}
    for swapCount > 0 {
        swapCount = 0
        for {
            if Less(item2, item1) { 
                bs.Swap(C,item2,item1)
                swapCount += 1
            }
        }
    }
}

Harap abaikan kesalahan apa pun... sama sekali belum teruji!

@mandolyte Saya tidak yakin apakah ini ditujukan kepada saya? Saya tidak benar-benar melihat perbedaan antara apa yang saya sarankan dan contoh Anda, kecuali Anda menggunakan antarmuka multi-parameter, dan saya memberikan contoh menggunakan tipe abstrak/terkait. Untuk lebih jelasnya, saya pikir Anda memerlukan antarmuka multi-parameter, dan tipe abstrak/terkait untuk generalisasi penuh, yang saat ini tidak didukung oleh Go.

Saya akan menyarankan antarmuka Anda kurang umum daripada yang saya usulkan, karena mereka mengikat urutan pengurutan, pola akses, dan aksesibilitas ke antarmuka yang sama, yang tentu saja akan menghasilkan proliferasi antarmuka, misalnya dua pesanan (kurang , lebih besar), tiga jenis akses (hanya-baca, hanya-tulis, dapat diubah) dan lima pola akses (terusan-single-pass, maju-multi-pass, dua arah, diindeks, acak) akan menghasilkan 36 antarmuka dibandingkan dengan hanya 11 jika masalah disimpan terpisah.

Anda dapat mendefinisikan antarmuka yang saya usulkan dengan antarmuka multi-parameter alih-alih tipe abstrak seperti ini:

type I ForwardIterator interface {
   successor(x I) I
}
type R V Readable interface {
   source(x R) V
}
type V Ord interface {
   less(x V, y V) : bool
}

Perhatikan bahwa satu-satunya yang membutuhkan dua parameter tipe adalah antarmuka Readable. Namun kami kehilangan kemampuan untuk objek iterator untuk 'mengandung' jenis objek yang diulang, yang merupakan masalah besar karena sekarang kami harus memindahkan tipe 'nilai' di dalam sistem tipe, dan kami harus memperbaikinya . Hal ini menyebabkan proliferasi parameter tipe yang tidak baik, dan meningkatkan kemungkinan kesalahan pengkodean. Kami juga kehilangan kemampuan untuk mendefinisikan 'DistanceType' pada iterator, yang merupakan tipe angka terkecil yang diperlukan untuk menghitung elemen dalam koleksi, yang berguna untuk pemetaan ke int8, int16, int32 dll, untuk memberikan tipe yang Anda butuhkan menghitung elemen tanpa overflow.

Ini terkait erat dengan konsep 'ketergantungan fungsional'. Jika suatu tipe secara fungsional bergantung pada tipe lain, itu harus menjadi tipe abstrak/terkait. Hanya jika kedua tipe tersebut independen, mereka akan menjadi parameter tipe yang terpisah.

Beberapa masalah:

  1. Tidak dapat menggunakan sintaks f(x I) saat ini untuk antarmuka multi-parameter. Saya tidak suka sintaks ini membingungkan antarmuka (yang merupakan batasan pada tipe) dengan tipe.
  2. Akan membutuhkan cara untuk mendeklarasikan tipe yang diparameterisasi.
  3. Perlu ada cara untuk mendeklarasikan tipe terkait untuk antarmuka dengan serangkaian parameter tipe tertentu.

@keean Tidak yakin saya mengerti bagaimana atau mengapa jumlah antarmuka menjadi sangat tinggi. Berikut adalah contoh kerja yang lengkap: https://play.folang.org/p/BZa6BdsfBgZ (berbasis irisan, bukan wadah umum, sehingga tidak diperlukan metode Next()).

Ini hanya menggunakan satu jenis struct, tidak ada antarmuka sama sekali. Saya harus menyediakan semua fungsi dan penutupan anonim (mungkin di situlah trade offnya?). Contoh ini menggunakan algoritme pengurutan gelembung yang sama untuk mengurutkan potongan bilangan bulat dan potongan Poin "(x,y)", di mana jarak dari asal adalah dasar dari fungsi Less().

Bagaimanapun, saya berharap untuk menunjukkan bagaimana memiliki fungsi dalam sistem tipe dapat membantu.

@mandolyte Saya pikir saya salah mengerti apa yang Anda usulkan. Saya melihat apa yang Anda bicarakan adalah "folang" yang sudah memiliki beberapa fitur pemrograman fungsional yang bagus yang ditambahkan ke Go. Apa yang telah Anda terapkan pada dasarnya adalah membuat kelas tipe multi-parameter dengan tangan. Anda meneruskan apa yang dikenal sebagai kamus fungsi ke fungsi sortir. Ini melakukan secara eksplisit apa yang akan dilakukan antarmuka secara implisit. Fitur semacam ini mungkin diperlukan sebelum antarmuka multi-parameter dan tipe terkait, tetapi Anda akhirnya mengalami masalah saat melewati semua kamus itu. Saya pikir antarmuka menyediakan kode yang lebih bersih dan lebih mudah dibaca.

Menyortir irisan adalah masalah yang terpecahkan. Berikut kode untuk quicksort.go slice yang diimplementasikan menggunakan bahasa go-li (golang ditingkatkan) .

func main(){
    var data = []int{5,3,1,8,9}

    Sort(data, func(a *int, b *int) int {
        return *a - *b
    })

    fmt.Println(data)
}

Anda dapat bereksperimen dengan ini di taman bermain

Contoh lengkap Anda dapat menempelkan ke taman bermain , karena mengimpor paket quicksort tidak berfungsi di taman bermain.

@go-li Saya yakin Anda dapat mengurutkan sepotong, akan sedikit buruk jika Anda tidak bisa. Intinya adalah secara umum Anda ingin dapat mengurutkan wadah linier apa pun dengan kode yang sama, sehingga Anda hanya perlu menulis algoritme pengurutan satu kali, apa pun wadah (struktur data) yang Anda urutkan dan apa pun jenisnya. konten adalah.

Ketika Anda dapat melakukan ini, pustaka standar dapat menyediakan fungsi penyortiran universal, dan tidak ada yang perlu menulisnya lagi. Ada dua manfaat untuk ini, lebih sedikit kesalahan karena lebih sulit daripada yang Anda pikirkan untuk menulis algoritma pengurutan yang benar, Stepanov menggunakan contoh bahwa sebagian besar programmer tidak dapat dengan benar mendefinisikan pasangan 'min' dan 'max', jadi apa harapan kita? benar untuk algoritma yang lebih kompleks. Manfaat lainnya adalah ketika hanya ada satu definisi dari setiap algoritma pengurutan, setiap peningkatan dalam kejelasan atau kinerja yang dapat dilakukan akan menguntungkan semua program yang menggunakannya. Orang dapat menghabiskan waktu mereka untuk mencoba meningkatkan algoritme umum daripada harus menulis sendiri untuk setiap tipe data yang berbeda.

@keean
Pertanyaan lain terkait dengan diskusi kita sebelumnya. Saya tidak tahu bagaimana seseorang dapat mendefinisikan fungsi pemetaan yang mengubah item dari iterable, mengembalikan tipe iterable beton baru yang itemnya mungkin dari tipe yang berbeda dari yang asli.

Dan saya membayangkan pengguna fungsi seperti itu ingin tipe konkret dikembalikan, bukan antarmuka lain.

@urandom Dengan asumsi kami tidak bermaksud melakukannya 'di tempat' yang tidak aman, yang Anda inginkan adalah fungsi peta yang memiliki 'read-iterator' dari satu jenis dan 'write-iterator' dari jenis lain, yang dapat didefinisikan sesuatu seperti:

map<I, O, U>(first I, last I, out O, fn U) requires
   ForwardIterator<I>, Readable<I>,
   ForwardIterator<O>, Writable<O>,
   UnaryFunction<U>, Domain(U) == ValueType(I), Codomain(U) == ValueType(O)

Untuk kejelasan, "ValueType" adalah jenis antarmuka yang terkait "Dapat dibaca" dan "Dapat ditulis", "Domain" dan "Codomain" adalah jenis antarmuka "UnaryFunction" yang terkait. Jelas sangat membantu jika kompiler dapat secara otomatis menurunkan antarmuka untuk tipe data seperti "UnaryFunction". Sementara ini tampak seperti refleksi, tidak, dan itu semua terjadi pada waktu kompilasi menggunakan tipe statis.

@keean Bagaimana memodelkan kendala Readable dan Writable tersebut dalam konteks antarmuka Go saat ini?

Maksud saya, ketika kita memiliki tipe A dan kita ingin mengonversi ke tipe B , tanda tangan dari UnaryFunction itu adalah func (input A) B (kan?), tapi bagaimana bisa? dimodelkan hanya menggunakan antarmuka dan bagaimana map generik itu (atau filter , reduce , dll.) akan dimodelkan untuk menjaga jenis pipa?

@geovanisouza92 Saya pikir "Jenis Keluarga" akan bekerja dengan baik karena mereka dapat diimplementasikan sebagai mekanisme ortogonal dalam sistem tipe, dan kemudian diintegrasikan ke dalam sintaks untuk antarmuka seperti yang dilakukan di Haskell.

Keluarga tipe seperti fungsi terbatas pada tipe (pemetaan). Karena implementasi antarmuka dipilih berdasarkan tipe, kami dapat menyediakan pemetaan tipe untuk setiap implementasi.

Jadi jika kita definisikan:

ValueType MyIntArrayIterator -> Int

Fungsi sedikit menipu tetapi fungsi memiliki tipe, misalnya:

fn(x : Int) Float

Kami akan menulis jenis ini:

Int -> Float

Penting untuk disadari bahwa -> hanyalah konstruktor tipe infix, seperti '[]' untuk Array adalah konstruktor tipe, kita bisa dengan mudah menulis ini;

Fn Int Float
Or
Fn<Int, Float>

Tergantung pada preferensi kami untuk jenis sintaks. Sekarang kita dapat dengan jelas melihat bagaimana kita dapat mendefinisikan:

Domain  Fn<Int, Float> -> Int
Codomain Fn<Int, Float> -> Float

Sekarang sementara kami dapat memberikan semua definisi ini dengan tangan, mereka dapat dengan mudah diturunkan oleh kompiler.

Mengingat keluarga tipe ini, kita dapat melihat bahwa definisi peta yang saya berikan di atas hanya membutuhkan tipe IO dan U untuk membuat instance generik, karena semua tipe lain secara fungsional bergantung pada ini. Kita bisa melihat tipe ini langsung disediakan oleh argumen.

Terima kasih @kean.

Ini akan berfungsi dengan baik untuk fungsi bawaan/yang telah ditentukan sebelumnya. Apakah Anda mengatakan konsep yang sama akan diterapkan untuk fungsi yang ditentukan pengguna atau lib userland?

"Jenis Keluarga" itu akan dibawa untuk runtime, dalam kasus beberapa konteks kesalahan?

Bagaimana dengan antarmuka kosong, sakelar ketik, dan refleksi?


EDIT: Saya hanya ingin tahu, tidak mengeluh.

@giovanisouza92 yah tidak ada yang berkomitmen Pergi untuk memiliki obat generik jadi saya berharap skeptisisme. Pendekatan saya adalah jika Anda akan melakukan obat generik, Anda harus melakukannya dengan benar.

Dalam contoh saya, 'peta' ditentukan oleh pengguna. Tidak ada yang istimewa tentangnya, dan di dalam fungsi tersebut Anda cukup menggunakan metode antarmuka yang Anda perlukan pada tipe tersebut persis seperti yang Anda lakukan di Go sekarang. Satu-satunya perbedaan adalah bahwa kita dapat meminta suatu tipe untuk memenuhi beberapa antarmuka, antarmuka dapat memiliki beberapa parameter tipe (walaupun contoh peta tidak menggunakan ini) dan ada juga tipe terkait (dan batasan pada tipe seperti kesetaraan tipe '==' tapi ini seperti persamaan Prolog dan menyatukan jenis). Inilah sebabnya mengapa ada sintaks yang berbeda untuk menentukan antarmuka yang diperlukan oleh suatu fungsi. Perhatikan ada perbedaan penting lainnya:

f(x I, y I) requires ForwardIterator<I>

Vs

f(x ForwardIterator, y ForwardIterator)

Perhatikan ada perbedaan di 'x' dan 'y' yang terakhir dapat menjadi tipe berbeda yang memenuhi antarmuka ForwardIterator, sedangkan di sintaks sebelumnya 'x' dan 'y' keduanya harus tipe yang sama (yang memenuhi iterator maju). Ini penting agar fungsi tidak terlalu dibatasi, dan memungkinkan jenis beton disebarkan lebih jauh selama kompilasi.

Saya tidak berpikir ada perubahan mengenai sakelar tipe dan refleksi, karena kami hanya memperluas konsep antarmuka. Karena go memiliki informasi tipe runtime, Anda tidak mengalami masalah yang sama seperti Haskell dan memerlukan tipe eksistensial.

Memikirkan tentang Go, polimorfisme runtime, dan keluarga tipe, kita mungkin ingin membatasi tipe-keluarga itu sendiri ke antarmuka untuk menghindari keharusan memperlakukan setiap tipe terkait sebagai antarmuka kosong saat runtime yang akan lambat.

Jadi mengingat pemikiran itu, saya akan memodifikasi proposal saya di atas sehingga ketika mendeklarasikan antarmuka Anda akan mendeklarasikan antarmuka/tipe untuk setiap tipe terkait bahwa semua implementasi antarmuka itu harus menyediakan tipe terkait yang memenuhi antarmuka itu. Dengan cara itu kita dapat mengetahui bahwa aman untuk memanggil metode apa pun dari antarmuka itu pada tipe terkait saat runtime tanpa harus mengetik-switch dari antarmuka kosong.

@keean
Demi memajukan perdebatan, izinkan saya menghapus kesalahpahaman yang saya rasakan mirip dengan sindrom yang tidak ditemukan di sini.

Iterator dua arah (dalam sintaks T func (*T) *[2]*T ) memiliki tipe func (*) *[2]* dalam sintaks go-li. Dengan kata lain, dibutuhkan pointer ke beberapa tipe dan mengembalikan pointer ke dua pointer ke elemen berikutnya dan sebelumnya dari tipe yang sama. Ini adalah tipe dasar beton dasar yang digunakan oleh daftar tertaut ganda .

Sekarang Anda dapat menulis apa yang Anda sebut peta, yang saya sebut fungsi generik foreach. Jangan salah ini berfungsi tidak hanya pada daftar tertaut tetapi juga pada apa pun yang memperlihatkan iterator dua arah!

func Foreach(link func(*) *[2]*, list **, direction byte, f func(*)) {

    if nil == *list {
        return
    }

    var end *
    end = *list

    var e *
    e = (*link(*list))[direction]
    f(end)

    for (e != end) && ((*link(e))[direction] != nil) {
        var newe = (*link(e))[direction]
        f(e)
        e = newe
    }
    return
}

Foreach dapat digunakan dalam dua cara, Anda menggunakannya dengan lambda dalam iterasi for-loop-like di atas elemen daftar atau koleksi.

const forward = 1
const backwards = 0
Foreach(iterator, collection, forward, func(element *element_type){
    // do something with every element
})

Atau Anda dapat menggunakannya untuk memetakan fungsi secara fungsional ke setiap elemen koleksi.

Foreach(iterator, collection, backwards, function_to_be_mapped_on_elements)

Iterator dua arah tentu saja dapat juga dimodelkan menggunakan antarmuka di go 1.
interface Iterator { Iter() [2]Iterator } Anda perlu memodelkannya menggunakan antarmuka untuk membungkus ("kotak") jenis yang mendasarinya. Pengguna Iterator kemudian mengetik menegaskan tipe yang diketahui setelah ia menemukan dan ingin mengunjungi elemen koleksi tertentu. Ini berpotensi mengkompilasi waktu tidak aman.

Apa yang Anda gambarkan selanjutnya adalah perbedaan antara pendekatan warisan dan pendekatan berbasis generik.

func modern(x func  (*) *[2]*, y func  (*) *[2]*){}

pendekatan ini mengkompilasi tipe waktu memeriksa bahwa kedua koleksi memiliki tipe dasar yang sama, dengan kata lain apakah iterator benar-benar mengembalikan tipe konkret yang sama

func modern_T_syntax<T>(x func  (*T) *[2]*T, y func  (*T) *[2]*T){}

Sama seperti di atas tetapi menggunakan T yang familier berarti ketik sintaks placeholder

func legacy(x Iterator, y Iterator){}

Dalam hal ini pengguna dapat meneruskan misalnya daftar tertaut integer sebagai x dan daftar tertaut mengambang sebagai y. Ini dapat menyebabkan potensi kesalahan run-time, kepanikan, atau dekoherensi internal lainnya, tetapi itu semua tergantung pada apa yang akan dilakukan warisan dengan dua iterator.

Sekarang kesalahpahaman. Anda mengklaim bahwa melakukan iterator dan melakukan pengurutan generik untuk mengurutkan iterator tersebut akan menjadi cara yang harus dilakukan. Itu akan menjadi hal yang sangat buruk untuk dilakukan, inilah alasannya

Iterator dan daftar tertaut adalah dua sisi dari mata uang yang sama. Bukti: koleksi apa pun yang mengekspos iterator hanya mengiklankan dirinya sebagai daftar tertaut. Katakanlah Anda perlu mengurutkannya. Apa yang harus dilakukan?

Jelas Anda menghapus daftar tertaut dari basis kode Anda dan menggantinya dengan pohon biner. Atau jika Anda ingin lebih suka menggunakan pohon pencarian seimbang seperti avl, merah-hitam, seperti yang diusulkan saya tidak tahu berapa tahun yang lalu oleh Ian dkk. Masih ini belum dilakukan secara umum di golang. Sekarang itu akan menjadi cara untuk pergi.

Solusi lain adalah dengan cepat dalam loop waktu O(N) di atas iterator, mengumpulkan pointer ke elemen ke dalam sepotong pointer generik, dilambangkan []*T dan mengurutkan pointer generik tersebut menggunakan pengurutan irisan yang buruk

Tolong beri ide orang lain kesempatan

@go-li Jika kita ingin menghindari sindrom yang tidak ditemukan di sini, kita harus melihat ke Alex Stepanov untuk definisi, karena ia cukup banyak menemukan pemrograman generik. Inilah cara saya mendefinisikannya, diambil dari "Elements of Programming" Stepanov halaman 111:

Bidirectional iterator<T> =
    ForwardIterator<T>
/\ predecessor : T -> T
/\ predecessor takes constant time
/\ (forall i in T) successor(i) is defined =>
        predecessor(successor(i)) is defined and equals i
/\ (forall i in T) predecessor(i) is defined =>
        successor(predecessor(i)) is defined and equals i

Ini tergantung pada definisi ForwardIterator:

ForwardIterator<T> =
    Iterator<T>
/\ regular_unary_function(successor)

Jadi pada dasarnya kami memiliki antarmuka yang mendeklarasikan fungsi successor dan fungsi predecessor , bersama dengan beberapa aksioma yang harus dipatuhi agar valid.

Mengenai legacy bukan berarti warisan akan salah, itu jelas tidak salah di Go saat ini, tetapi kompiler kehilangan peluang pengoptimalan, dan sistem tipe kehilangan kesempatan untuk menyebarkan tipe konkret lebih lanjut. Ini juga membatasi pemrogram untuk menentukan niat mereka secara tepat. Contohnya adalah fungsi identitas, yang saya maksudkan untuk mengembalikan dengan tepat jenis yang dilewatinya:

id(x T) T

Mungkin juga perlu disebutkan perbedaan antara tipe parametrik dan tipe yang dikuantifikasi secara universal. Tipe parametrik akan menjadi id<T>(x T) T sedangkan yang terkuantifikasi universal adalah id(x T) T (kita biasanya menghilangkan quantifier universal terluar dalam hal ini forall T ). Dengan tipe parametrik, sistem tipe harus memiliki tipe untuk T yang disediakan di callsite untuk id , dengan kuantifikasi universal yang tidak diperlukan selama T disatukan dengan tipe konkret sebelum kompilasi selesai. Cara lain untuk memahami bahwa fungsi parametrik bukanlah tipe tetapi templat untuk suatu tipe, dan itu hanya tipe yang valid setelah T diganti untuk tipe konkret. Dengan fungsi yang diukur secara universal id sebenarnya memiliki tipe forall T . T -> T yang dapat diteruskan oleh kompiler seperti Int .

@go-li

Jelas Anda menghapus daftar tertaut dari basis kode Anda dan menggantinya dengan pohon biner. Atau jika Anda ingin lebih suka menggunakan pohon pencarian seimbang seperti avl, merah-hitam, seperti yang diusulkan saya tidak tahu berapa tahun yang lalu oleh Ian dkk. Masih ini belum dilakukan secara umum di golang. Sekarang itu akan menjadi cara untuk pergi.

Memiliki struktur data yang terurut tidak berarti Anda tidak perlu mengurutkan data.

Jika kita ingin menghindari sindrom tidak-ditemukan-di sini kita harus melihat ke Alex Stepanov untuk definisi, karena ia cukup banyak menemukan pemrograman generik.

Saya akan menentang klaim apa pun bahwa pemrograman generik ditemukan oleh C++. Baca Liskov dkk. Kertas CACM 1977 jika Anda ingin melihat model awal pemrograman generik yang benar-benar berfungsi (type-safe, modular, no code bloat): https://dl.acm.org/citation.cfm?id=359789 (lihat Bagian 4 )

Saya pikir kita harus menghentikan diskusi ini dan menunggu tim golang (russ) datang dengan beberapa posting blog dan kemudian menerapkan solusi 👍 (lihat vgo) Mereka akan melakukannya

https://peter.bourgon.org/blog/2018/07/27/a-response-about-dep-and-vgo.html

Saya harap cerita ini menjadi peringatan bagi orang lain: jika Anda tertarik untuk memberikan kontribusi substansial pada proyek Go, tidak ada uji tuntas independen yang dapat mengimbangi desain yang tidak berasal dari tim inti.

Utas ini menunjukkan bagaimana tim inti tidak tertarik untuk berpartisipasi aktif dalam mencari solusi bersama komunitas.

Tapi pada akhirnya, jika mereka bisa membuat solusi sendiri lagi, tidak masalah bagi saya, selesaikan saja 👍

@andrewcmyers Yah mungkin "diciptakan" agak sulit, yang mungkin lebih seperti David Musser pada tahun 1971, yang kemudian bekerja dengan Stepanov pada beberapa perpustakaan umum untuk Ada.

Elemen Pemrograman bukan buku tentang C++, contohnya mungkin dalam C++ tetapi itu adalah hal yang sangat berbeda. Saya pikir buku ini adalah bacaan penting bagi siapa saja yang ingin menerapkan obat generik dalam bahasa apa pun. Sebelum mengabaikan Stepanov, Anda harus benar-benar membaca buku itu untuk melihat apa sebenarnya isinya.

Masalah ini sudah tegang di bawah batas skalabilitas GitHub. Harap tetap diskusi di sini terfokus pada isu-isu konkret untuk proposal Go.

Akan sangat disayangkan jika Go mengikuti rute C++.

@andrewcmyers Ya, saya dengan sepenuh hati setuju, tolong jangan gunakan C++ untuk saran sintaks atau sebagai patokan untuk melakukan sesuatu dengan benar. Sebagai gantinya, silakan lihat D untuk inspirasi .

@nomad-software

Saya suka D, sangat, tetapi apakah go membutuhkan fitur pemrograman meta waktu kompilasi yang kuat yang ditawarkan D?

Saya tidak suka sintaks template, juga di C++, yang berasal dari zaman batu.

Tapi bagaimana dengan ParametricType yang normalstandar yang ditemukan di Java atau C#, jika diperlukan juga dapat membebani ini dengan ParametricType

Dan lebih jauh lagi, saya tidak suka sintaks panggilan template di D dengan simbol bang, simbol bang lebih banyak digunakan saat ini untuk menunjukkan akses yang dapat diubah atau tidak dapat diubah untuk parameter suatu fungsi.

@nomad-software Saya tidak menyarankan bahwa sintaks C++ atau mekanisme templat adalah cara yang tepat untuk melakukan obat generik. Lebih dari itu "konsep" seperti yang didefinisikan oleh Stepanov memperlakukan tipe sebagai aljabar, yang merupakan cara yang sangat tepat untuk melakukan generik. Lihat kelas tipe Haskell untuk melihat bagaimana tampilannya. Kelas tipe Haskell sangat dekat dengan templat dan konsep c++ ​​secara semantik, jika Anda memahami apa yang sedang terjadi.

Jadi +1 untuk tidak mengikuti sintaks c++ dan +1 untuk tidak menerapkan sistem templat tipe-tidak aman :-)

@keean Alasan sintaks D adalah untuk menghindari <,> sama sekali dan mematuhi tata bahasa bebas konteks. Ini adalah bagian dari poin saya untuk menggunakan D sebagai inspirasi. <,> adalah pilihan yang sangat buruk untuk sintaks parameter generik.

@nomad-software Seperti yang saya tunjukkan di atas (dalam komentar yang sekarang tersembunyi) Anda perlu menentukan parameter tipe untuk tipe parametrik, tetapi tidak untuk tipe yang diukur secara universal (karenanya perbedaan antara Rust dan Haskell, cara penanganan tipe sebenarnya berbeda dalam sistem tipe). Juga konsep C++ == Haskell type-classes == Go interface, setidaknya pada tingkat konseptual.

Apakah sintaks D benar-benar lebih disukai:

auto add(T)(T lhs, T rhs) {
    return lhs + rhs;

Mengapa ini lebih baik daripada gaya C++/Java/Rust:

T add<T>(T lhs, T rhs) {
    return lhs + rhs;
}

Atau gaya Scala:

T add[T](T lhs, T rhs) {
    return lhs + rhs;
}

Saya telah melakukan beberapa pemikiran tentang sintaks untuk parameter tipe. Saya tidak pernah menjadi penggemar "kurung sudut" di C++ dan Java karena mereka membuat parsing cukup rumit dan dengan demikian menghambat pengembangan alat. Tanda kurung siku sebenarnya adalah pilihan klasik (dari CLU, Sistem F, dan bahasa awal lainnya dengan polimorfisme parametrik).

Namun, sintaks Go cukup sensitif, mungkin karena sudah sangat singkat. Kemungkinan sintaks berdasarkan tanda kurung siku atau kurung membuat ambiguitas tata bahasa bahkan lebih buruk daripada yang diperkenalkan oleh tanda kurung sudut. Jadi, terlepas dari kecenderungan saya, kurung sudut tampaknya menjadi pilihan terbaik untuk Go. (Tentu saja, ada juga kurung sudut nyata yang tidak akan membuat ambiguitas — — tetapi mereka akan memerlukan penggunaan karakter Unicode).

Tentu saja, sintaks yang tepat yang digunakan untuk parameter tipe kurang penting daripada mendapatkan semantik yang benar. Pada titik itu, bahasa C++ adalah model yang buruk. Pekerjaan kelompok penelitian saya tentang obat generik di Genus (PLDI 2015) dan Familia (OOPSLA 2017) menawarkan pendekatan lain yang memperluas kelas tipe dan menyatukannya dengan antarmuka.

@andrewcmyers Saya pikir kedua makalah itu menarik, tetapi saya akan mengatakan itu bukan arah yang baik untuk Go, karena Genus berorientasi objek, dan Go tidak, dan Familia menyatukan polimorfisme subtipe dan parametrik, dan Go tidak memiliki keduanya. Saya pikir Go harus mengadopsi polimorfisme parametrik atau kuantifikasi universal, itu tidak perlu subtipe, dan menurut saya adalah bahasa yang lebih baik untuk tidak memilikinya.

Saya pikir Go harus mencari obat generik yang tidak memerlukan orientasi objek dan tidak memerlukan subtipe. Go sudah memiliki antarmuka, yang menurut saya merupakan mekanisme yang bagus untuk obat generik. Jika Anda dapat melihat bahwa antarmuka Go == konsep c++ ​​== kelas tipe Haskell, bagi saya tampaknya cara untuk menambahkan obat generik sambil mempertahankan cita rasa 'Go' adalah dengan memperluas antarmuka untuk mengambil beberapa parameter tipe (saya akan seperti tipe terkait pada antarmuka juga, tetapi itu bisa menjadi ekstensi terpisah untuk membantu mendapatkan beberapa parameter tipe diterima). Itu akan menjadi perubahan utama, tetapi untuk mengaktifkan ini perlu ada sintaks 'alternatif' untuk antarmuka di tanda tangan fungsi, sehingga Anda bisa mendapatkan beberapa parameter tipe ke antarmuka, di situlah sintaks braket sudut keseluruhan masuk .

Antarmuka Go bukanlah kelas tipe — mereka hanyalah tipe — tetapi menyatukan antarmuka dengan kelas tipe adalah cara yang ditunjukkan Familia. Mekanisme Genus dan Familia tidak terikat pada bahasa yang sepenuhnya berorientasi objek. Antarmuka Go sudah membuat Go "berorientasi objek" dengan cara yang penting, jadi saya pikir idenya dapat diadaptasi dalam bentuk yang sedikit disederhanakan.

@andrewcmyers

Antarmuka Go bukan kelas tipe — mereka hanya tipe

Mereka tidak berperilaku seperti tipe bagi saya, karena mereka mengizinkan polimorfisme. Objek dalam array polimorfik seperti Addable[] masih memiliki tipe sebenarnya (terlihat oleh refleksi runtime), sehingga mereka berperilaku persis seperti kelas tipe parameter tunggal. Fakta bahwa mereka ditempatkan di tempat tipe di tanda tangan tipe hanyalah notasi singkatan yang menghilangkan variabel tipe. Jangan bingung notasi dengan semantik.

f(x : Addable) == f<T>(x : T) requires Addable<T>

Identitas ini tentu saja hanya berlaku untuk antarmuka parameter tunggal.

Satu-satunya perbedaan yang signifikan antara antarmuka dan kelas tipe parameter tunggal adalah bahwa antarmuka didefinisikan secara lokal, tetapi ini berguna karena menghindari masalah koherensi global yang dimiliki Haskell dengan kelas tipenya. Saya pikir ini adalah poin yang menarik dalam desain ruang. Antarmuka multi-parameter akan memberi Anda semua kekuatan kelas tipe multi-parameter dengan manfaat menjadi lokal. Tidak perlu menambahkan pewarisan atau subtipe apa pun ke bahasa Go (yang menurut saya merupakan dua fitur utama yang mendefinisikan OO).

MENURUT OPINI SAYA:

Masih memiliki tipe default akan lebih disukai daripada DSL yang didedikasikan untuk mengekspresikan batasan tipe. Seperti memiliki fungsi f(s T fmt.Stringer) yang merupakan fungsi umum yang menerima semua jenis yang juga/memuaskan antarmuka fmt.Stringer .

Dengan cara ini dimungkinkan untuk memiliki fungsi generik seperti:

func add(a, b T int) T int {
    return a + b
}

Sekarang fungsi add() bekerja dengan semua jenis T yang seperti int s mendukung operator + .

@dc0d Saya setuju bahwa tampaknya menarik melihat sintaks Go saat ini. Namun itu tidak 'lengkap' karena tidak dapat mewakili semua kendala yang diperlukan untuk obat generik, dan masih akan ada dorongan untuk memperluas ini lebih jauh. Ini akan menghasilkan proliferasi sintaks yang berbeda yang saya lihat bertentangan dengan tujuan kesederhanaan. Pandangan saya adalah bahwa kesederhanaan tidak sederhana, itu harus yang paling sederhana tetapi tetap menawarkan kekuatan ekspresif yang diperlukan. Saat ini saya melihat batasan utama Go dalam kekuatan ekspresif generik adalah kurangnya antarmuka multi-parameter. Misalnya antarmuka Koleksi dapat didefinisikan seperti:

type T U Collection interface {
   member(c T, v U) Bool
   insert(c T, v U) T
}

Jadi ini masuk akal bukan? Kami ingin menulis antarmuka di atas hal-hal seperti koleksi. Jadi pertanyaannya adalah bagaimana Anda menggunakan antarmuka ini dalam suatu fungsi. Saran saya akan seperti:

func[T, U] f(c T, e U) (Bool, T) requires Collection[T, U] {
   a := member(c, e)
   d := insert(c, e)
   return a, d
}

Sintaksnya hanya saran, tetapi saya tidak terlalu keberatan dengan sintaksisnya, selama Anda dapat mengekspresikan konsep-konsep ini dalam bahasa.

@keean Tidak akan akurat jika saya mengatakan saya tidak keberatan dengan sintaks sama sekali. Tetapi intinya adalah untuk menekankan memiliki tipe default untuk setiap parameter generik. Dalam hal itu contoh yang diberikan untuk antarmuka akan menjadi:

type Collection interface (T interface{}, U interface{}) {
   member(c T, v U) Bool
   insert(c T, v U) T
}

Sekarang bagian (T interface{}, U interface{}) membantu mendefinisikan batasan. Misalnya jika anggota dimaksudkan untuk memenuhi fmt.Stringer , maka definisinya adalah:

type Collection interface (T fmt.Stringer, U fmt.Stringer) {
   member(c T, v U) Bool
   insert(c T, v U) T
}

@dc0d Ini sekali lagi akan membatasi dalam arti bahwa Anda ingin membatasi lebih dari satu jenis parameter, pertimbangkan:

type OrderedCollection[T, U] interface
   requires Collection[T, U], Ord[U] {...}

Saya pikir saya melihat dari mana Anda berasal dengan penempatan parameter, Anda dapat memiliki:

type OrderedCollection interface(T, U)
   requires Collection(T, U), Ord(U) {...}

Seperti yang saya katakan, saya tidak terlalu sibuk dengan sintaks, karena saya bisa terbiasa dengan sebagian besar sintaks. Dari atas saya anggap Anda lebih suka tanda kurung '()' untuk antarmuka multi-parameter.

@keean Mari kita pertimbangkan antarmuka heap.Interface . Definisi saat ini di perpustakaan standar adalah:

type Interface interface {
    sort.Interface
    Push(x interface{}) // add x as element Len()
    Pop() interface{}   // remove and return element Len() - 1.
}

Sekarang mari kita tulis ulang sebagai antarmuka generik, menggunakan tipe default:

type Interface interface (T interface{}) {
    sort.Interface
    Push(x T) // add x as element Len()
    Pop() T   // remove and return element Len() - 1.
}

Ini tidak merusak seri kode Go 1.x di luar sana. Salah satu implementasinya adalah proposal saya untuk Type Alias ​​Rebinding. Tapi saya yakin bisa ada implementasi yang lebih baik.

Memiliki tipe default memungkinkan kita menulis kode generik yang dapat digunakan dengan kode gaya Go 1.x. Dan perpustakaan standar dapat menjadi perpustakaan umum, tanpa merusak apa pun. Itu kemenangan besar IMO.

@dc0d jadi Anda menyarankan peningkatan bertahap? Apa yang Anda sarankan terlihat baik bagi saya sebagai peningkatan bertahap, namun masih memiliki kekuatan ekspresif generik yang terbatas. Bagaimana Anda menerapkan antarmuka "Koleksi" dan "OrderedCollection"?

Pertimbangkan bahwa beberapa ekstensi bahasa parsial dapat menghasilkan produk akhir yang lebih kompleks (dengan beberapa sintaks alternatif) daripada mengimplementasikan solusi lengkap dengan cara paling sederhana yang Anda bisa.

@keean Saya tidak mengerti bagian requires Collection[T, U], Ord[U] . Bagaimana mereka membatasi parameter tipe T dan U ?

@dc0d Mereka bekerja dengan cara yang sama seperti dalam suatu fungsi, tetapi berlaku untuk semuanya. Jadi untuk setiap Pair dari tipe TU yang merupakan OrderedCollection kami mensyaratkan bahwa TU juga merupakan turunan dari Collection dan U adalah Ord. Jadi di mana pun kita menggunakan OrderedCollection, kita dapat menggunakan metode dari Collection dan Ord yang sesuai.

Jika kita sedang minimalis, ini tidak diperlukan, karena kita dapat menyertakan antarmuka tambahan dalam tipe fungsi yang kita butuhkan, misalnya:

type OrderedCollection interface(T, U)
{
   first(c T) U
}

func[T] first(c T[]) T requires Collection(T[], T), Ord T
{...}

func[T] f(c T[]) requires OrderedCollection(T[], T), Collection(T[], T), Ord(T)
{...}

Tapi ini mungkin lebih mudah dibaca:

type OrderedCollection interface(T, U) 
   requires Collection(T, U), Ord(U)
{
   first(c T) U
}

func[T] first(c T[]) T
{...}

func[T] f(c T[]) requires OrderedCollection(T[], T)
{...}

@keean (IMO) Selama ada nilai default wajib untuk parameter tipe, saya merasa senang. Dengan cara itu dimungkinkan untuk mempertahankan kompatibilitas mundur dengan seri kode Go 1.x. Itulah poin utama yang saya coba sampaikan.

@keean

Antarmuka Go bukan kelas tipe — mereka hanya tipe

Mereka tidak berperilaku seperti tipe bagi saya, karena mereka mengizinkan polimorfisme.

Ya, mereka mengizinkan polimorfisme subtipe . Go memiliki subtipe melalui tipe antarmuka. Itu tidak secara eksplisit mendeklarasikan hierarki subtipe, tetapi itu sebagian besar ortogonal. Apa yang membuat Go tidak sepenuhnya berorientasi objek adalah kurangnya pewarisan.

Atau, Anda dapat melihat antarmuka sebagai aplikasi kelas tipe yang dikuantifikasi secara eksistensial. Saya percaya itulah yang ada dalam pikiran Anda. Itulah yang kami lakukan di Genus dan Familia.

@andrewcmyers

Ya, mereka mengizinkan polimorfisme subtipe.

Sejauh yang saya tahu adalah invarian, tidak ada kovarians atau kontravarians, ini berbicara dengan kuat bahwa ini bukan subtipe. Sistem tipe polimorfik bersifat invarian, jadi bagi saya tampaknya Go lebih dekat dengan model ini, dan memperlakukan antarmuka sebagai kelas tipe parameter tunggal tampaknya lebih sejalan dengan kesederhanaan Go. Kurangnya kovarians dan kontravarian adalah manfaat besar bagi obat generik, lihat saja kebingungan yang dibuat oleh hal-hal seperti itu dalam bahasa seperti C#:

https://docs.microsoft.com/en-us/dotnet/standard/generics/covariance-and-contravariance

Saya pikir Go harus benar-benar menghindari kerumitan semacam ini. Bagi saya ini berarti bahwa kami tidak ingin obat generik dan subtipe dalam sistem tipe yang sama.

Atau, Anda dapat melihat antarmuka sebagai aplikasi kelas tipe yang dikuantifikasi secara eksistensial. Saya percaya itulah yang ada dalam pikiran Anda. Itulah yang kami lakukan di Genus dan Familia.

Karena Go memiliki informasi tipe saat runtime, tidak diperlukan kuantifikasi eksistensial. Di Haskell, tipe tidak memiliki kotak (seperti tipe 'C' asli) dan ini berarti setelah kita memasukkan sesuatu ke dalam koleksi eksistensial, kita tidak dapat (dengan mudah) memulihkan tipe konten, yang dapat kita lakukan hanyalah menggunakan antarmuka yang disediakan (kelas tipe ). Ini diimplementasikan dengan menyimpan pointer ke antarmuka di samping data mentah. Di Go, tipe data disimpan sebagai gantinya, datanya adalah 'Kotak' (seperti pada data C# boxed dan unboxed). Dengan demikian Go tidak terbatas hanya pada antarmuka yang disimpan dengan data karena dimungkinkan (dengan menggunakan tipe-case) untuk memulihkan tipe data dalam koleksi, yang hanya mungkin di Haskell dengan menerapkan 'Refleksi' typeclass (walaupun canggung untuk mengeluarkan data, dimungkinkan untuk membuat serial tipe dan data, untuk mengatakan string, dan kemudian deserialise di luar kotak eksistensial). Jadi kesimpulan yang saya miliki adalah bahwa antarmuka Go berperilaku persis seperti kelas tipe, jika Haskell menyediakan kelas tipe 'Refleksi' sebagai bawaan. Dengan demikian tidak ada kotak eksistensial, dan kita masih bisa mengetikkan huruf besar-kecil pada konten koleksi, namun antarmuka berperilaku persis seperti kelas tipe. Perbedaan antara Haskell dan Go adalah dalam semantik data kotak vs data tidak kotak, dan antarmuka adalah kelas tipe parameter tunggal. Akibatnya ketika 'Go' memperlakukan antarmuka sebagai tipe, apa yang sebenarnya dilakukannya adalah:

Addable[] == exists T . T[] requires Addable[T], Reflection[T]

Mungkin perlu dicatat bahwa ini adalah cara kerja "Objek Sifat" yang sama di Rust.

Go benar-benar dapat menghindari eksistensial (terlihat oleh programmer), kovarians dan kontravarians yang merupakan hal yang baik, dan itu akan membuat obat generik lebih sederhana dan lebih kuat menurut saya.

Sejauh yang saya tahu adalah invarian, tidak ada kovarians atau kontravarians, ini berbicara dengan kuat bahwa ini bukan subtipe.

Sistem tipe polimorfik bersifat invarian, jadi bagi saya tampaknya lebih dekat dengan model ini, dan memperlakukan antarmuka sebagai kelas tipe parameter tunggal tampaknya lebih sejalan dengan kesederhanaan Go.

Bolehkah saya menyarankan bahwa Anda berdua benar? Dalam antarmuka itu setara dengan kelas tipe, tetapi kelas tipe adalah bentuk subtipe. Definisi subtipe yang saya temukan sejauh ini semuanya sangat kabur dan tidak tepat dan bermuara pada "A adalah subtipe B, jika yang satu dapat diganti dengan yang lain". Yang, IMO, dapat dengan mudah diperdebatkan untuk dipenuhi oleh tipe class .

Perhatikan, bahwa argumen varians itu sendiri tidak benar-benar berfungsi IMO. Varians adalah properti dari konstruktor tipe, bukan bahasa. Dan cukup normal bahwa tidak semua konstruktor tipe dalam suatu bahasa adalah varian (misalnya, banyak bahasa dengan subtipe memiliki larik yang dapat diubah, yang harus invarian agar aman untuk tipe). Jadi saya tidak mengerti mengapa Anda tidak dapat membuat subtipe tanpa konstruktor tipe varian.

Juga, saya percaya diskusi ini agak terlalu luas untuk masalah di repositori Go. Ini seharusnya bukan tentang membahas seluk-beluk teori tipe, tetapi tentang jika dan bagaimana menambahkan obat generik ke Go.

@Merovius Variance adalah properti yang terkait dengan subtipe. Dalam bahasa tanpa subtipe, tidak ada perbedaan. Agar ada varians di tempat pertama Anda harus memiliki subtipe, yang memperkenalkan masalah kovarians/kontravarians untuk mengetik konstruktor. Namun Anda benar bahwa dalam bahasa dengan subtipe, dimungkinkan untuk memiliki semua tipe-konstruktor invarian.

Kelas tipe sangat jelas bukan subtipe, karena kelas tipe bukan tipe. Namun kita dapat melihat 'tipe antarmuka' di Go sebagai apa yang disebut Rust sebagai 'objek sifat' yang secara efektif merupakan tipe yang diturunkan dari kelas tipe.

Semantik Go tampaknya cocok dengan salah satu model saat ini, karena tidak memiliki varians, dan memiliki 'objek sifat' implisit. Jadi mungkin Go berada pada titik kritis, generik dan sistem tipe dapat dikembangkan di sepanjang garis subtipe, memperkenalkan varians dan berakhir dengan sesuatu seperti generik di C#. Atau Go dapat memperkenalkan antarmuka multi-parameter, memungkinkan antarmuka untuk Koleksi, dan ini akan memutuskan hubungan langsung antara antarmuka dan 'tipe antarmuka'. Misalnya jika Anda memiliki:

type (T, U) Collection interface {
    member : (c T, e U) Bool
    insert: (c T, e U) T
}

member(c int32[], e int32) Bool {...}
insert(c int32[], e int32) int32[] {...}

member(c float32[], e float32) Bool {...}
insert(c float32[], e float32) float32[] {...}

Tidak ada lagi hubungan subtipe yang jelas antara tipe T, U dan Koleksi antarmuka. Jadi Anda hanya dapat melihat hubungan antara tipe instans dan tipe antarmuka sebagai subtipe untuk kasus khusus antarmuka parameter tunggal, dan kami tidak dapat mengekspresikan abstraksi hal-hal seperti koleksi dengan antarmuka parameter tunggal.

Saya pikir untuk obat generik Anda jelas harus dapat memodelkan hal-hal seperti koleksi, jadi antarmuka multi-parameter harus dimiliki bagi saya. Namun saya pikir interaksi antara kovarians dan kontravarians dalam obat generik menciptakan sistem tipe yang terlalu kompleks, jadi saya ingin menghindari subtipe.

@keean Karena antarmuka dapat digunakan sebagai tipe, dan kelas tipe bukan tipe, penjelasan paling alami dari semantik Go adalah bahwa antarmuka bukan kelas tipe. Saya mengerti bahwa Anda berdebat untuk menggeneralisasi antarmuka sebagai kelas tipe; Saya pikir itu adalah arah yang masuk akal untuk mengambil bahasa, dan sebenarnya kami telah mengeksplorasi pendekatan itu secara ekstensif dalam karya kami yang diterbitkan.

Mengenai apakah Go memiliki subtipe, harap pertimbangkan kode berikut:

package main

type Cloneable interface {
    Clone() Cloneable
}

type CloneableZ interface {
    Clone() Cloneable
    zero() int
}

type S struct {}

func (t S) Clone() Cloneable {
    c := t
    return c
}

func (t S) zero() int {
    return 0
}

var x CloneableZ = S{}
var y Cloneable = x

func main() {
    print("ok\n")
}

Penetapan dari x ke y menunjukkan bahwa tipe y dapat digunakan di mana tipe x diharapkan. Ini adalah hubungan subtipe, yaitu: CloneableZ <: Cloneable , dan juga S <: CloneableZ . Bahkan jika Anda menjelaskan antarmuka dalam hal kelas tipe, masih akan ada hubungan subtipe yang berperan di sini, seperti S <: ∃T.CloneableZ[T] <: ∃T.Cloneable[T] .

Perhatikan bahwa akan sangat aman bagi Go untuk mengizinkan fungsi Clone untuk mengembalikan S , tetapi Go kebetulan memberlakukan aturan pembatasan yang tidak perlu untuk kesesuaian dengan antarmuka: pada kenyataannya, aturan yang sama dengan Java awalnya diberlakukan. Subtipe tidak memerlukan konstruktor tipe non-invarian, seperti yang diamati @Merovius .

@andrewcmyers Apa yang terjadi dengan antarmuka multi-parameter, seperti yang diperlukan untuk koleksi abstrak?

Selanjutnya penugasan dari x ke y dapat dilihat sebagai mendemonstrasikan pewarisan antarmuka tanpa subtipe sama sekali. Di Haskell (yang jelas tidak memiliki subtipe) Anda akan menulis:

class Cloneable t => CloneableZ t where...

Di mana kita memiliki x adalah tipe yang mengimplementasikan CloneableZ yang menurut definisi juga mengimplementasikan Cloneable , jadi jelas dapat ditugaskan ke y .

Untuk mencoba dan meringkas, Anda dapat melihat antarmuka sebagai tipe dan Go memiliki subtipe terbatas tanpa konstruktor tipe kovarian atau kontravarian, atau Anda dapat melihatnya sebagai "objek sifat", atau mungkin di Go kami menyebutnya " objek antarmuka", yang secara efektif merupakan wadah polimorfik yang dibatasi oleh "kelas tipe" antarmuka. Dalam model kelas tipe tidak ada subtipe, dan karena itu tidak ada alasan untuk memikirkan kovarians dan kontravarians.

Jika kita tetap menggunakan model subtipe, kita tidak dapat memiliki tipe koleksi, inilah mengapa C++ harus memperkenalkan template, karena subtipe berorientasi objek tidak cukup untuk mendefinisikan konsep secara umum seperti wadah. Kami berakhir dengan dua mekanisme untuk abstraksi, objek dan subtipe, dan templat/sifat dan generik, dan interaksi antara keduanya menjadi kompleks, lihat C++, C#, dan Scala sebagai contoh. Akan ada panggilan lanjutan untuk memperkenalkan konstruktor kovarian dan kontravarian untuk meningkatkan kekuatan generik, sejalan dengan bahasa lain tersebut.

Jika kita menginginkan koleksi generik tanpa memperkenalkan sistem generik yang terpisah, maka kita harus memikirkan antarmuka seperti kelas tipe. Antarmuka multi-parameter berarti tidak lagi memikirkan subtipe, dan sebaliknya memikirkan pewarisan antarmuka. Jika kami ingin meningkatkan obat generik di Go, dan mengizinkan abstraksi hal-hal seperti koleksi, dan kami tidak ingin kerumitan sistem tipe bahasa seperti C++, C#, Scala dll, maka antarmuka multi-parameter, dan pewarisan antarmuka adalah caranya untuk pergi.

@keean

Apa yang terjadi dengan antarmuka multi-parameter, seperti yang diperlukan untuk koleksi abstrak?

Silakan lihat makalah kami tentang Genus dan Familia, yang mendukung batasan tipe multiparameter. Familia menyatukan kendala tersebut dengan antarmuka dan memungkinkan antarmuka untuk membatasi beberapa jenis.

Jika kita tetap menggunakan model subtipe, kita tidak dapat memiliki tipe koleksi

Saya tidak sepenuhnya yakin apa yang Anda maksud dengan "model subtipe", tetapi cukup jelas bahwa Java dan C# memiliki tipe koleksi, jadi klaim ini tidak masuk akal bagi saya.

Di mana kita memiliki x adalah tipe yang mengimplementasikan CloneableZ yang menurut definisi juga mengimplementasikan Cloneable, jadi jelas dapat ditugaskan ke y.

Tidak, dalam contoh saya, x adalah variabel dan y adalah variabel lain. Jika saya tahu bahwa y adalah beberapa tipe CloneableZ dan x adalah beberapa tipe Cloneable , itu tidak berarti bahwa saya dapat menetapkan dari y ke x. Itulah yang dilakukan oleh contoh saya.

Untuk memperjelas bahwa subtipe diperlukan untuk memodelkan Go, di bawah ini adalah versi yang dipertajam dari contoh yang padanan moralnya tidak mengetik-periksa di Haskell. Contoh menunjukkan bahwa subtyping memungkinkan pembuatan koleksi heterogen di mana elemen yang berbeda memiliki implementasi yang berbeda. Selanjutnya, rangkaian implementasi yang mungkin bersifat terbuka.

type Cloneable interface {
    Clone() Cloneable
}

type CloneableZ interface {
    Clone() Cloneable
    zero() int
}

type S struct {}

func (t S) Clone() Cloneable {
    c := t
    return c
}

type T struct { x int }

func (t T) Clone() Cloneable {
    c := t
    return c
}

func (t S) zero() int {
    return 0
}

var x CloneableZ = S{}
var y Cloneable = T{}
var a [2]Cloneable = [2]Cloneable{x, y}

@andrewcmyers

Saya tidak sepenuhnya yakin apa yang Anda maksud dengan "model subtipe", tetapi cukup jelas bahwa Java dan C# memiliki tipe koleksi, jadi klaim ini tidak masuk akal bagi saya.

Lihat mengapa C++ mengembangkan templat, model subtipe OO tidak mampu mengekspresikan konsep umum yang diperlukan untuk menggeneralisasi hal-hal seperti koleksi. C# dan Java juga harus memperkenalkan sistem generik lengkap yang terpisah dari objek, subtipe dan pewarisan, dan kemudian harus membersihkan kekacauan interaksi kompleks kedua sistem dengan hal-hal seperti konstruktor tipe kovarian dan kontravarian. Dengan melihat ke belakang, kita dapat menghindari subtipe OO, dan sebagai gantinya melihat apa yang terjadi jika kita menambahkan antarmuka (kelas tipe) ke bahasa yang diketik sederhana. Inilah yang telah dilakukan Rust sehingga layak untuk dilihat, tetapi tentu saja ini rumit oleh hal seumur hidup. Go memiliki GC sehingga tidak akan memiliki kompleksitas itu. Saran saya adalah bahwa Go dapat diperluas untuk memungkinkan antarmuka multi-parameter, dan menghindari kerumitan ini.

Mengenai klaim Anda bahwa Anda tidak dapat melakukan contoh ini di Haskell, berikut adalah kodenya:

{-# LANGUAGE ExistentialQuantification #-}

class ICloneable t where
    clone :: t -> t

class ICloneable t => ICloneableZ t where
    zero :: t

data S = S deriving Show

instance ICloneable S where
    clone x = x

data T = T Int deriving Show

instance ICloneable T where
    clone x = x

instance ICloneableZ T where
    zero = T 0

data Cloneable = forall a . (ICloneable a, Show a) => ToCloneable a

instance Show Cloneable where
    show (ToCloneable x) = show x

main = do
    x <- return S
    y <- return (T 27)
    a <- return [ToCloneable x, ToCloneable y]
    putStrLn (show a)

Beberapa perbedaan menarik, Go secara otomatis menurunkan tipe ini data Cloneable = forall a . (ICloneable a, Show a) => ToCloneable a karena ini adalah cara Anda mengubah antarmuka (yang tidak memiliki penyimpanan) menjadi tipe (yang memiliki penyimpanan), Rust juga menurunkan tipe ini dan menyebutnya "objek sifat" . Dalam bahasa lain seperti Java, C# dan Scala, kami menemukan Anda tidak dapat membuat instance antarmuka, yang sebenarnya "benar", antarmuka bukan tipe, mereka tidak memiliki penyimpanan, Go menurunkan tipe wadah eksistensial secara otomatis untuk Anda sehingga Anda dapat memperlakukan antarmuka seperti tipe, dan Go menyembunyikannya dari Anda dengan memberi wadah eksistensial nama yang sama dengan antarmuka asalnya. Hal lain yang perlu diperhatikan adalah bahwa [2]Cloneable{x, y} ini memaksa semua anggota untuk Cloneable , sedangkan Haskell tidak memiliki paksaan implisit seperti itu, dan kita harus secara eksplisit memaksa anggota dengan ToCloneable .

Juga telah ditunjukkan kepada saya bahwa kita tidak boleh mempertimbangkan subtipe S dan T dari Cloneable karena S dan T tidak kompatibel secara struktural. Kami benar-benar dapat mendeklarasikan jenis apa pun sebagai turunan dari Cloneable (hanya dengan mendeklarasikan definisi yang relevan dari fungsi clone di Go) dan jenis tersebut tidak perlu memiliki hubungan satu sama lain sama sekali.

Sebagian besar proposal untuk Generik tampaknya menyertakan token tambahan yang menurut saya mengganggu keterbacaan dan perasaan Go yang sederhana. Saya ingin mengusulkan sintaks yang berbeda yang saya pikir mungkin bisa bekerja dengan baik tata bahasa Go yang ada (bahkan terjadi pada sintaks highlight cukup baik di Github Markdown).

Poin-poin utama dari proposal:

  • Tata bahasa Go tampaknya selalu memiliki cara mudah untuk menentukan kapan deklarasi tipe telah berakhir karena ada beberapa token atau kata kunci tertentu yang kami cari. Jika ini benar dalam semua kasus, argumen tipe dapat dengan mudah ditambahkan mengikuti nama tipe itu sendiri.
  • Seperti kebanyakan proposal, pengidentifikasi yang sama berarti tipe yang sama dalam deklarasi fungsi apa pun. Pengidentifikasi ini tidak pernah lepas dari deklarasi.
  • Di sebagian besar proposal, Anda harus mendeklarasikan argumen tipe generik, tetapi dalam proposal ini tersirat. Beberapa orang akan mengklaim ini menyakitkan keterbacaan atau kejelasan (implisitnya buruk), atau membatasi kemampuan untuk menyebutkan jenis, sanggahan berikut:

    • Ketika datang untuk menyakiti keterbacaan, saya pikir Anda bisa memperdebatkannya dengan cara apa pun, ekstraatau [T] merusak keterbacaan sama banyaknya dengan membuat banyak gangguan sintaksis.

    • Implisititas bila digunakan dengan benar dapat membantu bahasa menjadi kurang bertele-tele. Kami menghindari deklarasi tipe dengan := sepanjang waktu karena informasi yang disembunyikan oleh itu tidak cukup penting untuk dijabarkan setiap saat.

    • Memberi nama tipe konkret (non-generik) a atau t mungkin merupakan praktik yang buruk, jadi proposal ini menganggap aman untuk mencadangkan pengidentifikasi ini untuk bertindak sebagai argumen tipe generik. Meskipun ini mungkin memerlukan migrasi perbaikan?

package main

import "fmt"

type LinkedList a struct {
  Head *Node a
  Tail *Node a
}

type Node a {
  Next *Node a
  Prev *Node a

  Value a
}

func main() {
  // Not sure about how recursive we could get with the inference
  ll := LinkedList string {
    // The string bit could be inferred
    Head: Node string { Value: "hello world" },
  }
}

func (l *LinkedList a) Append(value a) {
  newNode := &Node{Value: value}

  if l.Tail == nil {
    l.Head = newNode
    l.Tail = l.Head
    return
  }

  l.Tail.Next = newNode
  l.Tail = l.Tail.Next
}

Ini diambil dari Intisari yang memiliki sedikit lebih banyak detail serta jenis jumlah yang diusulkan di sini: https://Gist.github.com/aarondl/9b950373642fcf5072942cf0fca2c3a2

Ini bukan proposal Generik yang sepenuhnya dihapus dan tidak dimaksudkan demikian, ada banyak masalah yang harus diselesaikan untuk dapat menambahkan obat generik ke Go. Yang ini hanya menangani sintaks, dan saya berharap kita dapat berdiskusi tentang apakah yang diusulkan layak/diinginkan atau tidak.

@aarondl
Terlihat bagus bagi saya, menggunakan sintaks ini kita akan memiliki:

type Collection a b interface {
   member(c a, e b) Bool
   insert(c a, e b) a
}

func insert(c *LinkedList a, e a) *LinkedList a {
   c.Append(e)
   return c
}

@keean Tolong jelaskan jenis Collection sedikit. Saya gagal memahaminya:

type Collection a b interface {
   member(c a, e b) Bool
   insert(c a, e b) a
}

@dc0d Collection adalah antarmuka yang mengabstraksi _all_ collections, jadi pohon, daftar, irisan dll, sehingga kita dapat memiliki operasi umum seperti member dan insert yang akan bekerja pada koleksi apa pun yang berisi tipe data apa pun. Pada contoh di atas saya berikan contoh pendefinisian 'insert' untuk tipe LinkedList pada contoh sebelumnya:

func insert(c *LinkedList a, e a) *LinkedList a {
   c.Append(e)
   return c
}

Kita juga bisa mendefinisikannya untuk sebuah slice

func insert(c []a, e a) []a {
   return append(c, e)
}

Namun kami bahkan tidak memerlukan jenis fungsi parametrik dengan variabel tipe seperti yang diilustrasikan oleh @aarondl dengan tipe polimorfik a agar ini berfungsi, karena Anda dapat menentukan untuk tipe konkret:

func insert(c *LinkedList int, e int) *LinkedList int {
   c.Append(e)
   return c
}

func insert(c *LinkedList float, e float) *LinkedList float {
   c.Append(e)
   return c
}

func insert(c int[], e int) int[] {
   return append(c, e)
}

func insert(c float[], e float) float[] {
   return append(c, e)
}

Jadi Collection adalah antarmuka untuk menggeneralisasi kedua jenis wadah dan jenis isinya, memungkinkan fungsi generik ditulis yang beroperasi pada semua kombinasi wadah dan konten.

Tidak ada alasan Anda juga tidak dapat memiliki sepotong koleksi []Collection di mana semua isinya akan menjadi tipe koleksi yang berbeda dengan tipe nilai yang berbeda, memberikan member dan insert ditentukan untuk setiap kombinasi .

@aarondl Mengingat bahwa type LinkedList a sudah merupakan deklarasi tipe yang valid, saya hanya dapat melihat dua cara untuk membuat ini dapat diuraikan dengan jelas: Membuat konteks tata bahasa menjadi sensitif (masuk ke masalah penguraian C, ugh) atau menggunakan lookahead tak terbatas ( yang cenderung dihindari oleh tata bahasa go, karena pesan kesalahan yang buruk dalam kasus kegagalan). Saya mungkin salah memahami sesuatu, tetapi IMO yang menentang pendekatan tanpa token.

@keean Antarmuka di Go menggunakan metode, bukan fungsi. Dalam sintaks spesifik yang Anda sarankan, tidak ada yang melampirkan insert ke *LinkedList untuk kompiler (dalam Haskell yang dilakukan melalui deklarasi instance ). Itu juga normal untuk metode untuk mengubah nilai yang mereka operasikan. Tak satu pun dari ini adalah Show-Stopper, hanya menunjukkan bahwa sintaks yang Anda sarankan tidak bekerja dengan baik dengan Go. Mungkin lebih seperti

type Collection e interface {
    Element(e) book
    Insert(e)
}

func (l *(LinkedList e)) Element(el e) book {
    // ...
}

func (l* (LinkedList e)) Insert(el e) {
    // ...
}

Yang juga menunjukkan beberapa pertanyaan lagi sehubungan dengan bagaimana parameter tipe dicakup dan bagaimana ini harus diuraikan.

@aarondl ada juga lebih banyak pertanyaan yang saya miliki tentang proposal Anda. Misalnya, itu tidak mengizinkan batasan, jadi Anda hanya mendapatkan polimorfisme tanpa batasan. Yang, secara umum, tidak terlalu berguna, karena Anda tidak diizinkan untuk melakukan apa pun dengan nilai yang Anda peroleh (misalnya, Anda tidak dapat mengimplementasikan Koleksi dengan peta, karena tidak semua jenis adalah kunci peta yang valid). Apa yang harus terjadi ketika seseorang mencoba melakukan hal seperti itu? Jika ini adalah kesalahan waktu kompilasi, apakah itu mengeluh tentang instantiasi (pesan kesalahan C++ di depan) atau pada definisi (pada dasarnya Anda tidak dapat melakukan apa pun, karena tidak ada yang berfungsi dengan semua jenis)?

@keean Masih saya gagal memahami bagaimana a dibatasi untuk menjadi daftar (atau irisan atau koleksi lainnya). Apakah ini tata bahasa khusus yang bergantung pada konteks untuk koleksi? Jika demikian apa nilainya? Tidak mungkin mendeklarasikan tipe yang ditentukan pengguna dengan cara ini.

@Merovius Apakah itu berarti Go tidak dapat melakukan pengiriman ganda, dan membuat argumen pertama dari 'fungsi' menjadi spesial? Ini menunjukkan bahwa tipe terkait akan lebih cocok daripada antarmuka multi-parameter. Sesuatu seperti ini:

type Collection interface {
   type Element
   Member(e Element) Bool
   Insert(e Element) Collection
}

type IntSlice struct {
    value []Int,
}

type IntSlice.Element = Int

func (IntSlice) Member(e Int) Bool {...}
func (IntSlice) Insert(e Int) IntSlice {...}

func useIt(c Collection, e Collection.Element) {...}

Namun ini masih memiliki masalah karena tidak ada yang membatasi kedua koleksi untuk menjadi tipe yang sama ... Anda akhirnya membutuhkan sesuatu seperti:

func[A] useIt(c A, e A.Element) requires A:Collection

Untuk mencoba menjelaskan perbedaannya, antarmuka multi-parameter memiliki tipe _input_ tambahan yang mengambil bagian dalam pemilihan instance (karenanya koneksi dengan beberapa pengiriman), sedangkan tipe terkait adalah tipe _output_, hanya tipe penerima yang mengambil bagian dalam pemilihan instance, dan kemudian jenis terkait tergantung pada jenis penerima.

@dc0d a dan b adalah parameter tipe antarmuka, sama seperti di kelas tipe Haskell. Agar sesuatu dapat dianggap sebagai Collection ia harus mendefinisikan metode yang cocok dengan tipe di antarmuka di mana a dan b dapat berupa tipe apa pun. Namun seperti yang telah ditunjukkan oleh @Merovius , antarmuka Go berbasis metode, dan tidak mendukung banyak pengiriman sehingga antarmuka multi-parameter mungkin tidak cocok. Dengan model metode pengiriman tunggal Go, kemudian memiliki tipe terkait di antarmuka, alih-alih beberapa parameter tampaknya lebih cocok. Namun kurangnya pengiriman ganda membuat fungsi implementasi seperti unify(x, y) sulit, dan Anda harus menggunakan pola pengiriman ganda yang tidak terlalu bagus.

Untuk menjelaskan hal multi-parameter sedikit lebih jauh:

type Cloneable[A] interface {
   clone(x A) A
}

Di sini a singkatan dari semua jenis, kami tidak peduli apa itu, selama fungsi yang benar didefinisikan, kami menganggapnya Cloneable . Kami akan mempertimbangkan antarmuka sebagai batasan pada tipe daripada tipe itu sendiri.

func clone(x int) int {...}

jadi dalam kasus 'clone' kita mengganti a untuk int dalam definisi antarmuka, dan kita dapat memanggil clone jika substitusi berhasil. Ini sangat cocok dengan notasi ini:

func[A] test(x A) A requires Cloneable[A] {...}

Ini setara dengan:

type Cloneable interface {
   clone() Cloneable
}

tetapi mendeklarasikan fungsi bukan metode, dan dapat diperluas dengan beberapa parameter. Jika Anda memiliki bahasa dengan banyak pengiriman, tidak ada yang istimewa tentang argumen pertama dari suatu fungsi/metode, jadi mengapa menulisnya di tempat yang berbeda.

Karena Go tidak memiliki banyak pengiriman, ini semua mulai terasa seperti terlalu banyak untuk diubah sekaligus. Sepertinya tipe terkait akan lebih cocok, meskipun lebih terbatas. Ini akan memungkinkan koleksi abstrak, tetapi bukan solusi elegan untuk hal-hal seperti penyatuan.

@Merovius Terima kasih telah melihat proposal. Biarkan saya mencoba untuk mengatasi kekhawatiran Anda. Saya sedih Anda tidak menyukai proposal sebelum kita membahasnya lebih lanjut, saya harap saya dapat mengubah pikiran Anda - atau mungkin Anda dapat mengubah saya :)

Pandangan ke depan tanpa batas:
Jadi seperti yang saya sebutkan dalam proposal, saat ini sepertinya tata bahasa Go memiliki cara yang baik untuk mendeteksi "akhir" dari hampir semua hal secara sintaksis. Dan kami masih melakukannya karena argumen generik implisit. Huruf kecil tunggal menjadi konstruksi sintaksis yang menciptakan argumen generik itu - atau apa pun yang kami putuskan untuk membuat token sebaris itu, mungkin kami bahkan mundur ke hal yang di-token seperti @a dalam proposal jika kami cukup menyukai sintaksnya tetapi tidak mungkin diberikan kesulitan kompiler tanpa token, meskipun proposal kehilangan banyak pesona segera setelah Anda melakukannya.

Terlepas dari masalah type LinkedList a di bawah proposal ini tidak terlalu sulit karena kita tahu bahwa a adalah argumen tipe generik dan jadi ini akan gagal dengan kesalahan kompiler yang sama seperti type LinkedList gagal hari ini dengan: prog.go:3:16: expected type, found newline (and 1 more errors) . Posting asli tidak benar-benar keluar dan mengatakannya tetapi Anda tidak diizinkan untuk menyebutkan tipe konkret [a-z]{1} lagi yang saya -pikir- memecahkan masalah ini dan merupakan pengorbanan saya pikir kita semua akan baik-baik saja pembuatan (Saya hanya dapat melihat kerugian dalam membuat tipe nyata dengan nama huruf tunggal dalam kode Go hari ini).

Itu hanya polimorfisme tanpa batas
Alasan saya menghilangkan segala jenis ciri atau batasan argumen generik adalah karena saya merasa itulah peran antarmuka di Go, jika Anda ingin melakukan sesuatu dengan nilai maka nilai itu harus berupa tipe antarmuka dan bukan tipe generik sepenuhnya. Saya pikir proposal ini juga cocok dengan antarmuka.

Di bawah proposal ini, kami masih memiliki masalah yang sama seperti yang kami lakukan sekarang dengan operator seperti + sehingga Anda tidak dapat membuat fungsi add generik untuk semua tipe numerik, tetapi Anda dapat menerima fungsi add generik sebagai argumen. Pertimbangkan hal berikut:

func Sort(slice []a, compare func (a, a) bool) { ... }

Pertanyaan tentang pelingkupan

Anda memberi contoh di sini:

type Collection e interface {
    Element(e) book
    Insert(e)
}

func (l *(LinkedList e)) Element(el e) book {
    // ...
}

func (l* (LinkedList e)) Insert(el e) {
    // ...
}

Cakupan pengidentifikasi ini sebagai aturan terikat pada deklarasi/definisi tertentu tempat mereka berada. Mereka tidak dibagikan di mana pun dan saya tidak melihat alasan untuk itu.

@keean Itu sangat menarik meskipun seperti yang telah ditunjukkan orang lain, Anda harus mengubah apa yang telah Anda tunjukkan di sana untuk benar-benar dapat mengimplementasikan antarmuka (saat ini dalam contoh Anda tidak ada metode dengan penerima, hanya fungsi). Mencoba lebih memikirkan bagaimana hal ini memengaruhi proposal awal saya.

Huruf kecil satu huruf menjadi konstruksi sintaksis yang menciptakan argumen generik itu

Saya tidak merasa baik tentang itu; itu membutuhkan produksi terpisah untuk apa pengidentifikasi tergantung pada konteks dan juga berarti secara sewenang-wenang melarang pengidentifikasi tertentu untuk jenis. Tapi ini bukan waktu yang tepat untuk membicarakan detail ini.

Di bawah proposal ini kami masih memiliki masalah yang sama seperti yang kami lakukan sekarang dengan operator seperti +

Saya tidak mengerti kalimat ini. Saat ini, operator + tidak memiliki masalah tersebut, karena jenis operandnya diketahui secara lokal dan pesan kesalahannya jelas dan tidak ambigu serta menunjukkan sumber masalahnya. Apakah saya benar dalam mengasumsikan bahwa Anda mengatakan bahwa Anda ingin melarang penggunaan nilai generik apa pun yang tidak diizinkan untuk semua jenis yang mungkin (saya tidak dapat memikirkan banyak operasi seperti itu)? Dan buat kesalahan kompiler untuk ekspresi yang menyinggung dalam fungsi generik? IMO yang akan membatasi nilai obat generik terlalu banyak.

jika Anda ingin melakukan sesuatu dengan nilai maka nilai itu harus berupa tipe antarmuka dan bukan tipe generik sepenuhnya.

Dua alasan utama orang menginginkan obat generik, adalah kinerja (menghindari pembungkusan antarmuka) dan keamanan jenis (memastikan bahwa jenis yang sama digunakan di tempat yang berbeda, sementara tidak peduli yang mana itu). Hal ini tampaknya mengabaikan alasan tersebut.

anda bisa menerima fungsi add generik sebagai argumen.

Benar. Tapi cukup tidak ergonomis. Pertimbangkan berapa banyak keluhan di sana tentang sort API. Untuk banyak wadah generik, jumlah fungsi yang harus diterapkan dan diteruskan oleh pemanggil tampaknya menjadi penghalang. Pertimbangkan, bagaimana implementasi container/heap di bawah proposal ini dan bagaimana itu akan lebih baik daripada implementasi saat ini, dalam hal ergonomi? Tampaknya, kemenangannya dapat diabaikan di sini, paling banter. Anda harus menerapkan lebih banyak fungsi sepele (dan duplikat ke/referensi di setiap situs penggunaan), tidak lebih sedikit.

@Merovius

memikirkan hal ini dari @aarondl

anda bisa menerima fungsi add generik sebagai argumen.

Akan lebih baik untuk memiliki antarmuka Addable untuk memungkinkan penambahan yang berlebihan, mengingat beberapa sintaks untuk mendefinisikan operator infix:

type Addable interface {
   + (x Addable, y Addable) Addable
}

Sayangnya ini tidak berhasil, karena tidak menyatakan bahwa kami mengharapkan semua tipenya sama. Untuk mendefinisikan addable, kita memerlukan sesuatu seperti antarmuka multi-parameter:

type Addable[A] interface {
   + (x A, y A) A
}

Maka Anda juga perlu Go untuk melakukan pengiriman ganda yang berarti semua argumen dalam suatu fungsi diperlakukan seperti penerima untuk pencocokan antarmuka. Jadi dalam contoh di atas, jenis apa pun adalah Addable jika ada fungsi + yang didefinisikan di dalamnya yang memenuhi definisi fungsi dalam definisi antarmuka.

Tetapi mengingat perubahan itu, Anda sekarang dapat menulis:

type S struct {
   value: int
}

func (+) (x S, y S) S {
   return S {
      value: x.value + y.value
   }
}

func main() {
    println(S {value: 27} + S {value: 5})
}

Tentu saja fungsi overloading dan multiple-dispatch mungkin sesuatu yang orang tidak pernah inginkan di Go, tetapi kemudian hal-hal seperti mendefinisikan aritmatika dasar pada tipe yang ditentukan pengguna seperti vektor, matriks, bilangan kompleks dll, akan selalu mustahil. Seperti yang saya katakan di atas 'tipe terkait' pada antarmuka akan memungkinkan beberapa peningkatan kemampuan pemrograman generik, tetapi tidak secara umum penuh. Apakah pengiriman ganda (dan mungkin fungsi kelebihan beban) sesuatu yang bisa terjadi di Go?

hal-hal seperti mendefinisikan aritmatika dasar pada tipe yang ditentukan pengguna seperti vektor, matriks, bilangan kompleks dll, akan selalu mustahil.

Beberapa orang mungkin menganggap itu fitur :) AFAIR ada beberapa proposal atau utas yang beredar di suatu tempat membahas apakah itu harus. FWIW, saya pikir ini - lagi - mengembara di luar topik. Kelebihan operator (atau ide umum "cara membuat Go lebih Haskell") bukanlah inti dari masalah ini :)

Apakah pengiriman ganda (dan mungkin fungsi kelebihan beban) sesuatu yang bisa terjadi di Go?

Tidak pernah berkata tidak. Saya tidak mengharapkannya, secara pribadi.

@Merovius

Beberapa mungkin menganggap itu sebagai fitur :)

Tentu, dan jika Go tidak melakukannya, ada bahasa lain yang akan :-) Go tidak harus menjadi segalanya bagi semua orang. Saya hanya mencoba membuat beberapa ruang lingkup untuk obat generik di Go. Fokus saya adalah membuat bahasa yang sepenuhnya generik, karena saya tidak suka mengulangi diri saya sendiri dan boilerplate (dan saya tidak suka makro). Jika saya punya satu sen untuk setiap kali saya harus menulis daftar tertaut atau pohon di 'C' untuk beberapa tipe data tertentu. Ini sebenarnya membuat beberapa proyek tidak mungkin untuk tim kecil karena volume kode yang perlu disimpan di kepala Anda untuk memahaminya, dan kemudian dipertahankan melalui perubahan. Terkadang saya berpikir bahwa orang yang tidak membutuhkan obat generik hanya belum menulis program yang cukup besar. Tentu saja Anda dapat memiliki tim besar pengembang yang mengerjakan sesuatu dan hanya masing-masing pengembang yang bertanggung jawab atas sebagian kecil dari total kode, tetapi saya tertarik untuk membuat satu pengembang (atau tim kecil) seefektif mungkin.

Mengingat bahwa fungsi overloading dan multiple-dispatch berada di luar cakupan, dan juga mengingat masalah parsing dengan saran @aarondl , tampaknya menambahkan tipe terkait ke antarmuka, dan parameter tipe ke fungsi akan sejauh yang Anda inginkan. pergi dengan obat generik di Go.

Sesuatu seperti ini tampaknya menjadi hal yang benar:

type Collection interface {
   type Element
   Member(e Element) Bool
   Insert(e Element) Collection
}

type IntSlice struct {
    value []Int,
}

type IntSlice.Element = Int

func (IntSlice) Member(e Int) Bool {...}
func (IntSlice) Insert(e Int) IntSlice {...}

func useIt<T>(c T, e T.Element) requires T:Collection {...}

Kemudian akan ada keputusan dalam implementasi apakah akan menggunakan tipe parametrik atau tipe yang dikuantifikasi secara universal. Dengan tipe parametrik (seperti Java) maka fungsi 'generik' sebenarnya bukan fungsi tetapi semacam templat fungsi yang aman untuk tipe, dan karena itu tidak dapat diteruskan sebagai argumen kecuali jika parameter tipenya disediakan:

f(useIt) // not okay with parametric types
f(useIt<List>) // okay with parametric types

Dengan tipe yang dikuantifikasi secara universal, Anda dapat meneruskan useIt sebagai argumen, dan kemudian dapat diberikan dengan parameter tipe di dalam f . Alasan untuk mendukung tipe parametrik adalah karena Anda dapat membuat monomorfisme polimorfisme pada waktu kompilasi yang berarti tidak ada elaborasi fungsi polimorfik saat runtime. Saya tidak yakin ini masalah dengan Go, karena Go sudah melakukan pengiriman runtime pada antarmuka, jadi selama parameter type untuk useIt mengimplementasikan Koleksi, Anda dapat mengirim ke penerima yang benar saat runtime, jadi universal kuantifikasi mungkin cara yang tepat untuk Go.

Saya ingin tahu, SFINAE hanya disebutkan oleh @bcmils. Bahkan tidak disebutkan dalam proposal (meskipun Urutkan ada sebagai contoh).
Bagaimana tampilan Sort untuk slice dan linkedlist?

@keean
Saya tidak tahu bagaimana mendefinisikan koleksi 'Irisan' generik dengan saran Anda. Anda tampaknya mendefinisikan 'IntSlice' yang mungkin mengimplementasikan 'Collection' (meskipun Insert mengembalikan tipe yang berbeda dari yang diinginkan oleh antarmuka), tetapi itu bukan 'slice' generik, karena tampaknya hanya untuk int , dan implementasi metode hanya untuk int. Apakah kita perlu mendefinisikan implementasi spesifik per jenis?

Terkadang saya berpikir bahwa orang yang tidak membutuhkan obat generik hanya belum menulis program yang cukup besar.

Saya dapat meyakinkan Anda bahwa kesan itu salah. Dan FWIW, ISTM bahwa "sisi lain" menempatkan "tidak melihat kebutuhan" ke dalam ember yang sama dengan "tidak melihat kegunaan". Saya melihat kegunaannya dan tidak membantahnya. Saya tidak benar-benar melihat kebutuhannya . Saya baik-baik saja tanpa, bahkan dalam basis kode besar.

Dan jangan salah mengartikan "menginginkan mereka dilakukan dengan benar dan menunjukkan di mana proposal yang ada tidak" dengan "secara fundamental menentang gagasan itu".

juga diberikan masalah penguraian dengan saran @aarondl .

Seperti yang saya katakan, saya tidak berpikir berbicara tentang masalah parsing benar-benar produktif sekarang. Masalah parsing dapat diselesaikan. Lach polimorfisme dibatasi jauh lebih serius, semantik. IMO, menambahkan obat generik tanpa itu tidak benar-benar sepadan dengan usaha.

@urandom

Saya tidak tahu bagaimana mendefinisikan koleksi 'Irisan' generik dengan saran Anda.

Seperti yang diberikan di atas, Anda masih perlu mendefinisikan implementasi terpisah untuk setiap jenis irisan, namun Anda masih akan memperoleh keuntungan dari kemampuan menulis algoritme dalam istilah antarmuka generik. Jika Anda ingin mengizinkan implementasi umum untuk semua irisan, Anda harus mengizinkan jenis dan metode terkait parametrik. Catatan Saya memindahkan parameter tipe ke setelah kata kunci sehingga muncul sebelum tipe penerima.

type<T> []T.Element = Int

func<T> ([]T) Member(e T) Bool {...}
func<T> ([]T) Insert(e T) Collection {...}

Namun sekarang Anda juga harus berurusan dengan spesialisasi, karena seseorang dapat menentukan jenis dan metode terkait untuk []int yang lebih khusus dan Anda harus berurusan dengan yang mana yang akan digunakan. Biasanya Anda akan menggunakan contoh yang lebih spesifik, tetapi itu menambah lapisan kerumitan lainnya.

Saya tidak yakin seberapa banyak ini benar-benar menguntungkan Anda. Dengan contoh asli saya di atas, Anda dapat menulis algoritme generik untuk bertindak pada koleksi umum menggunakan antarmuka, dan Anda hanya perlu menyediakan metode dan tipe terkait untuk tipe yang sebenarnya Anda gunakan. Kemenangan utama bagi saya adalah mampu mendefinisikan algoritma seperti mengurutkan pada koleksi arbitrer dan menempatkan algoritma tersebut di perpustakaan. Jika saya kemudian memiliki daftar "bentuk", saya hanya perlu mendefinisikan metode antarmuka koleksi untuk daftar bentuk saya, dan saya kemudian dapat menggunakan algoritma apa pun di perpustakaan pada mereka. Mampu mendefinisikan metode antarmuka untuk semua jenis irisan kurang menarik bagi saya, dan mungkin terlalu rumit untuk Go?

@Merovius

Saya tidak benar-benar melihat kebutuhannya. Saya baik-baik saja tanpa, bahkan dalam basis kode besar.

Jika Anda dapat mengatasi 100.000 baris program, maka Anda akan dapat melakukan lebih banyak dengan 100.000 baris generik daripada 100.000 baris non-generik (karena pengulangan). Jadi Anda mungkin pengembang super-bintang yang mampu mengatasi basis kode yang sangat besar, tetapi Anda masih akan mencapai lebih banyak dengan basis kode generik yang sangat besar karena Anda akan menghilangkan redundansi. Program generik itu akan berkembang menjadi program non-generik yang lebih besar. Sepertinya bagi saya Anda belum mencapai batas kompleksitas Anda.

Namun saya pikir Anda benar 'kebutuhan' terlalu kuat, saya dengan senang hati menulis kode go, dengan hanya sesekali frustrasi tentang kurangnya obat generik, dan saya dapat mengatasi ini hanya dengan menulis lebih banyak kode, dan di Go kode itu sangat langsung dan literal.

Kurangnya polimorfisme dibatasi jauh lebih serius, semantik. IMO, menambahkan obat generik tanpa itu tidak benar-benar sepadan dengan usaha.

Saya setuju dengan ini.

Anda akan dapat melakukan lebih banyak dengan 100.000 baris generik daripada dengan 100.000 baris non-generik (karena pengulangan)

Saya ingin tahu, dari contoh hipotetis Anda, berapa % dari garis-garis itu yang akan menjadi fungsi umum?
Dalam pengalaman saya ini kurang dari 2% (dari basis kode dengan 115k LOC), jadi saya rasa ini bukan argumen yang bagus kecuali Anda menulis perpustakaan untuk "koleksi"

Saya berharap kita akhirnya mendapatkan obat generik tho

@keean

Mengenai klaim Anda bahwa Anda tidak dapat melakukan contoh ini di Haskell, berikut adalah kodenya:

Kode ini secara moral tidak setara dengan kode yang saya tulis. Ini memperkenalkan jenis pembungkus Cloneable baru selain antarmuka ICloneable. Kode Go tidak memerlukan pembungkus; begitu pula bahasa lain yang mendukung subtipe.

@andrewcmyers

Kode ini secara moral tidak setara dengan kode yang saya tulis. Ini memperkenalkan jenis pembungkus Cloneable baru selain antarmuka ICloneable.

Bukankah ini yang dilakukan kode ini:

type Cloneable interface {...}

Ini menginduksi tipe data 'Cloneable' yang berasal dari antarmuka. Anda tidak melihat 'ICloneable' karena Anda tidak memiliki deklarasi instance untuk antarmuka, Anda cukup mendeklarasikan metodenya.

Bisakah Anda menganggapnya sebagai subtipe ketika tipe yang mengimplementasikan antarmuka tidak harus kompatibel secara struktural?

@keean Saya akan menganggap Cloneable hanya sebagai tipe, bukan "tipe data". Dalam bahasa seperti Java, pada dasarnya tidak ada biaya tambahan untuk abstraksi Cloneable , karena tidak akan ada pembungkus, tidak seperti dalam kode Anda.

Sepertinya saya membatasi dan tidak diinginkan untuk memerlukan kesamaan struktural antara tipe yang mengimplementasikan antarmuka, jadi saya bingung tentang apa yang Anda pikirkan di sini.

@andrewcmyers
Saya menggunakan tipe dan tipe data secara bergantian. Setiap tipe yang dapat berisi data adalah tipe data.

karena tidak akan ada pembungkus, tidak seperti dalam kode Anda.

Selalu ada pembungkus karena tipe Go selalu berbentuk kotak, jadi pembungkus ada di sekitar semuanya. Haskell membutuhkan pembungkusnya secara eksplisit karena memiliki tipe yang tidak dikotak-kotakkan.

kesamaan struktural antara tipe yang mengimplementasikan antarmuka, jadi saya bingung tentang apa yang Anda pikirkan di sini.

Subtipe struktural membutuhkan tipe yang 'kompatibel secara struktural'. Karena tidak ada hierarki tipe eksplisit seperti dalam bahasa OO dengan pewarisan, subtipe tidak boleh nominal, jadi harus struktural, jika ada sama sekali.

Saya mengerti maksud Anda, yang akan saya gambarkan sebagai mempertimbangkan antarmuka sebagai kelas dasar abstrak, bukan antarmuka, dengan semacam hubungan subtipe nominal implisit dengan tipe apa pun yang mengimplementasikan metode yang diperlukan.

Saya benar-benar berpikir Go cocok dengan kedua model sekarang, dan itu bisa berjalan baik dari sini, tetapi saya menyarankan bahwa menyebutnya sebagai antarmuka bukan kelas menyarankan cara berpikir non-subtipe.

@keean Saya tidak mengerti komentar Anda. Pertama, Anda memberi tahu saya bahwa Anda tidak setuju dan bahwa saya "belum memenuhi batas kerumitan saya" dan kemudian Anda memberi tahu saya bahwa Anda setuju (dalam kata "kebutuhan" itu terlalu kuat). Saya juga berpikir argumen Anda keliru (Anda menganggap LOC adalah ukuran utama kompleksitas dan bahwa setiap baris kode sama). Tapi yang terpenting, saya tidak berpikir "siapa yang menulis program yang lebih rumit" benar-benar diskusi yang produktif. Saya hanya mencoba mengklarifikasi, bahwa argumen "jika Anda tidak setuju dengan saya, itu berarti Anda tidak mengerjakan masalah yang sulit atau menarik" tidak meyakinkan dan tidak menunjukkan itikad baik. Saya harap Anda dapat mempercayai bahwa orang-orang dapat tidak setuju dengan Anda tentang pentingnya fitur ini sambil sama-sama kompeten dan melakukan hal-hal yang sama menariknya.

@merovius
Saya mengatakan Anda mungkin seorang programmer yang lebih mampu daripada saya, dan dengan demikian dapat bekerja dengan lebih banyak kompleksitas. Saya tentu tidak berpikir Anda sedang mengerjakan masalah yang kurang menarik atau kurang kompleks, dan saya minta maaf karena menemukan cara itu. Saya menghabiskan kemarin mencoba membuat pemindai berfungsi, yang merupakan masalah yang sangat tidak menarik.

Saya dapat berpikir bahwa obat generik membantu saya menulis program yang lebih kompleks dengan kemampuan otak saya yang terbatas, dan juga mengakui bahwa saya tidak "membutuhkan" obat generik. Ini masalah derajat. Saya masih dapat memprogram tanpa obat generik, tetapi saya belum tentu dapat menulis perangkat lunak dengan kompleksitas yang sama.

Saya harap meyakinkan Anda bahwa saya bertindak dengan itikad baik, saya tidak memiliki agenda tersembunyi di sini, dan jika Go tidak mengadopsi obat generik, saya akan tetap menggunakannya. Saya memiliki pendapat tentang cara terbaik untuk melakukan obat generik, tetapi itu bukan satu-satunya pendapat, saya hanya dapat berbicara dari pengalaman saya sendiri. Jika saya tidak membantu, ada banyak hal lain yang dapat saya gunakan untuk menghabiskan waktu saya, jadi katakan saja, dan saya akan memfokuskan kembali di tempat lain.

@Merovius Terima kasih atas dialog lanjutannya.

| Dua alasan utama orang menginginkan obat generik, adalah kinerja (menghindari pembungkusan antarmuka) dan keamanan jenis (memastikan bahwa jenis yang sama digunakan di tempat yang berbeda, sementara tidak peduli yang mana itu). Hal ini tampaknya mengabaikan alasan tersebut.

Mungkin kita melihat apa yang saya usulkan dengan sangat berbeda, karena dari sudut pandang saya ia melakukan kedua hal ini sejauh yang saya tahu? Dalam contoh daftar tertaut tidak ada pembungkus dengan antarmuka dan oleh karena itu harus berkinerja baik seolah-olah ditulis tangan untuk jenis tertentu. Di sisi tipe-keamanan itu sama. Apakah ada contoh tandingan yang dapat Anda berikan di sini untuk membantu saya memahami dari mana Anda berasal?

| Benar. Tapi cukup tidak ergonomis. Pertimbangkan berapa banyak keluhan di sana tentang jenis API. Untuk banyak wadah generik, jumlah fungsi yang harus diterapkan dan diteruskan oleh pemanggil tampaknya menjadi penghalang. Pertimbangkan, bagaimana tampilan implementasi container/heap di bawah proposal ini dan bagaimana itu akan lebih baik daripada implementasi saat ini, dalam hal ergonomi? Tampaknya, kemenangannya dapat diabaikan di sini, paling banter. Anda harus menerapkan lebih banyak fungsi sepele (dan duplikat ke/referensi di setiap situs penggunaan), tidak lebih sedikit.

Saya sebenarnya tidak mempermasalahkan ini sama sekali. Saya tidak percaya bahwa jumlah fungsi akan menjadi penghalang tetapi saya pasti terbuka untuk melihat beberapa contoh tandingan. Ingatlah bahwa API yang dikeluhkan orang bukanlah API yang harus Anda sediakan fungsinya tetapi yang asli di sini: https://golang.org/pkg/sort/#Interface tempat Anda perlu membuat tipe baru yang sederhana slice + ketik Anda, dan kemudian terapkan 3 metode di atasnya. Mengingat keluhan dan rasa sakit yang terkait dengan antarmuka ini, berikut ini dibuat: https://golang.org/pkg/sort/#Slice , saya tidak memiliki masalah dengan API ini dan kami akan memulihkan penalti kinerja ini di bawah proposal yang sedang kita diskusikan hanya dengan mengubah definisi menjadi func Slice(slice []a, less func(a, a) bool) .

Dalam hal struktur data container/heap apa pun proposal Generik yang Anda terima yang memerlukan penulisan ulang keseluruhan. container/heap seperti paket sort hanya menyediakan algoritme di atas struktur data Anda sendiri, tetapi tidak ada paket yang pernah memiliki struktur data karena jika tidak, kita akan memiliki []interface{} dan biaya yang terkait dengan itu. Agaknya kami akan mengubahnya karena Anda akan dapat memiliki Heap yang memiliki irisan dengan tipe konkret berkat obat generik, dan ini berlaku di bawah salah satu proposal yang pernah saya lihat di sini (termasuk milik saya) .

Saya mencoba untuk memisahkan perbedaan dalam perspektif kami tentang apa yang saya usulkan. Dan saya pikir akar dari ketidaksepakatan (melewati preferensi pribadi secara sintaksis) adalah bahwa tidak ada batasan pada tipe Generik. Tapi saya masih mencoba untuk mencari tahu apa yang menguntungkan kita. Jika jawabannya adalah tidak ada yang menyangkut kinerja yang diizinkan untuk menggunakan antarmuka maka tidak banyak yang bisa saya katakan di sini.

Perhatikan definisi tabel hash berikut:

// Hasher turns a key into a hash
type Hasher interface {
  func Hash() []byte
}

type HashTable v struct {
   Keys   []Hasher
   Values []v
}

// Note that the generic arguments must be repeated here and immediately
// understood without reading another line of code, which to me
// is a readability win over the sudden appearance of the K and V which are
// defined elsewhere in the code in the example below. This is of course because
// the tokenized type declarations with constraints are fairly painful in general
// and repeating them everywhere is simply too much.
func (h (*HashTable v)) Insert(key Hasher, value v) { ... }

Apakah kita mengatakan bahwa []Hasher adalah non-starter karena masalah kinerja/penyimpanan dan bahwa untuk memiliki implementasi Generik yang sukses di Go, kita harus memiliki sesuatu seperti berikut ini?

// Without selecting another proposal I have no idea how the constraint might be defined or implemented so let's just pretend
type [K: Hasher, V] HashTable a struct {
   Keys   []K
   Values []V
}

func (h *HashTable) Insert(key K, value V) { ... }

Semoga Anda melihat dari mana saya berasal. Tapi mungkin saja saya tidak mengerti batasan yang ingin Anda terapkan pada kode tertentu. Mungkin ada kasus penggunaan yang belum saya pertimbangkan, terlepas dari itu, saya berharap untuk mendapatkan pemahaman yang lebih lengkap tentang apa persyaratannya dan bagaimana proposal itu gagal.

Mungkin kita melihat apa yang saya usulkan dengan sangat berbeda, karena dari sudut pandang saya ia melakukan kedua hal ini sejauh yang saya tahu?

"Ini" di bagian yang Anda kutip mengacu pada penggunaan antarmuka. Masalahnya bukan, proposal Anda juga tidak berfungsi, proposal Anda tidak mengizinkan polimorfisme terbatas, yang mengecualikan sebagian besar penggunaan untuk polimorfisme tersebut. Dan alternatif yang Anda sarankan untuk di mana antarmuka, yang juga tidak benar-benar membahas kasus penggunaan inti untuk obat generik (karena dua hal yang saya sebutkan).

Misalnya, proposal Anda (seperti yang ditulis sebelumnya) sebenarnya tidak mengizinkan penulisan peta generik dalam bentuk apa pun, karena itu akan membutuhkan setidaknya untuk dapat membandingkan kunci menggunakan == (yang merupakan kendala, jadi menerapkan a peta membutuhkan polimorfisme terbatas).

Mengingat keluhan dan rasa sakit yang terkait dengan antarmuka ini, berikut ini dibuat: https://golang.org/pkg/sort/#Slice

Perhatikan, bahwa antarmuka ini masih tidak mungkin dalam proposal generik Anda, karena bergantung pada refleksi untuk panjang dan pertukaran (jadi, sekali lagi, Anda memiliki kendala pada operasi irisan). Meskipun kami menerima API tersebut sebagai batas bawah dari apa yang harus dapat dicapai oleh obat generik (banyak orang tidak akan melakukannya. Masih banyak keluhan tentang kurangnya keamanan jenis di API itu), proposal Anda tidak akan lolos bar itu.

Tetapi juga, sekali lagi, Anda mengutip respons ke poin tertentu yang Anda buat, yaitu bahwa Anda bisa mendapatkan polimorfisme terbatas dengan meneruskan literal fungsi di API. Dan cara spesifik yang Anda sarankan untuk mengatasi kurangnya polimorfisme yang dibatasi akan membutuhkan penerapan API lama yang kurang lebih. yaitu Anda mengutip tanggapan saya untuk argumen ini, yang kemudian Anda ulangi:

kami akan memulihkan penalti kinerja ini di bawah proposal yang sedang kami diskusikan hanya dengan mengubah definisi menjadi func Slice(slice []a, less func(a, a) bool).

Itu adalah API lama. Anda mengatakan "proposal saya tidak mengizinkan polimorfisme terbatas, tetapi itu tidak masalah, karena kami tidak dapat menggunakan obat generik dan sebagai gantinya menggunakan solusi yang ada (refleksi/antarmuka) sebagai gantinya". Nah, menanggapi "proposal Anda tidak mengizinkan kasus penggunaan paling dasar yang orang inginkan untuk obat generik" dengan "kita bisa melakukan hal-hal yang sudah dilakukan orang tanpa obat generik untuk kasus penggunaan paling dasar" sepertinya tidak membuat kita mengerti dimana saja, tbh. Proposal generik yang tidak membantu Anda menulis bahkan tipe wadah dasar, sortir, maks... sepertinya tidak sepadan.

ini benar di bawah salah satu proposal yang pernah saya lihat di sini (termasuk milik saya).

Sebagian besar proposal generik menyertakan beberapa cara untuk membatasi parameter tipe. yaitu untuk menyatakan "parameter tipe harus memiliki metode Less", atau "parameter tipe harus sebanding". Milik Anda - AFAICT - tidak.

Perhatikan definisi tabel hash berikut:

Definisi Anda tidak lengkap. a) Jenis kunci juga membutuhkan kesetaraan dan b) Anda tidak mencegah penggunaan jenis kunci yang berbeda. yaitu ini akan menjadi hukum:

type hasherA uint64

func (a hasherA) Hash() []byte {
    b := make([]byte, 8)
    binary.BigEndian.PutUint64(b, uint64(a))
    return b
}

type hasherB string

func (b hasherB) Hash() []byte {
    return []byte(b)
}

h := new(HashTable int)
h.Insert(hasherA(42), 1)
h.Insert(hasherB("Hello world"), 2)

Seharusnya tidak legal, karena Anda menggunakan jenis kunci yang berbeda. yaitu wadah tidak dicentang jenisnya ke tingkat yang diinginkan orang. Anda perlu membuat parameter hashtable pada tipe kunci dan nilai

type HashTable k v struct {
    Keys []k
    Values []v
}

func (h *(HashTable k v)) Insert(key k, value v) {
    // You can't actually do anything with k, as it's unconstrained. i.e. you can't hash it, compare it…
    // Implementing this is impossible in your proposal.
}

// If it weren't impossible, you'd get this:
h := new(HashTable hasherA int)
h[hasherA(42)] = 1
h[hasherB("Hello world")] = 2 // compile error - can't use hasherB as hasherA

Atau, jika membantu, bayangkan Anda mencoba mengimplementasikan hash-set. Anda akan mendapatkan masalah yang sama tetapi sekarang wadah yang dihasilkan tidak memiliki pemeriksaan jenis tambahan apa pun atas interface{} .

Inilah sebabnya mengapa proposal Anda tidak membahas kasus penggunaan yang paling mendasar: Ini bergantung pada antarmuka untuk membatasi polimorfisme, tetapi kemudian tidak benar-benar menyediakan cara apa pun untuk memeriksa konsistensi antarmuka tersebut. Anda dapat memiliki pemeriksaan tipe yang konsisten atau memiliki polimorfisme yang dibatasi, tetapi tidak keduanya. Tapi Anda membutuhkan keduanya.

bahwa untuk memiliki implementasi Generics yang sukses di Go, kita harus memiliki sesuatu seperti berikut ini?

Setidaknya itulah yang saya rasakan tentang itu, ya, cukup banyak. Jika sebuah proposal tidak mengizinkan penulisan wadah atau penyortiran yang aman untuk tipe atau… itu tidak benar-benar menambahkan apa pun ke bahasa yang ada yang cukup signifikan untuk membenarkan biaya.

@Merovius Oke. Saya pikir saya punya pemahaman tentang apa yang Anda inginkan. Perlu diingat bahwa kasus penggunaan Anda sangat jauh dari yang saya inginkan. Saya tidak benar-benar gatal untuk jenis wadah yang aman meskipun saya curiga - seperti yang Anda nyatakan - itu mungkin pendapat minoritas. Beberapa hal terbesar yang ingin saya lihat adalah jenis hasil alih-alih kesalahan dan manipulasi irisan yang mudah tanpa duplikasi atau refleksi di mana pun yang proposal saya melakukan pekerjaan yang wajar untuk mengatasi. Namun, saya dapat melihat bagaimana dari sudut pandang Anda "tidak membahas kasus penggunaan paling dasar" jika kasus penggunaan dasar Anda menulis wadah generik tanpa menggunakan antarmuka,

Perhatikan, bahwa antarmuka ini masih tidak mungkin dalam proposal obat generik Anda, karena bergantung pada refleksi untuk panjang dan pertukaran (jadi, sekali lagi, Anda memiliki kendala pada operasi irisan). Meskipun kami menerima API tersebut sebagai batas bawah dari apa yang harus dapat dicapai oleh obat generik (banyak orang tidak akan melakukannya. Masih banyak keluhan tentang kurangnya keamanan jenis di API itu), proposal Anda tidak akan lolos bar itu.

Membaca ini, jelas Anda telah benar-benar salah memahami cara irisan generik akan/seharusnya bekerja di bawah proposal ini. Melalui kesalahpahaman inilah Anda sampai pada kesimpulan yang salah bahwa "antarmuka ini masih tidak memungkinkan dalam proposal Anda". Di bawah proposal apa pun, irisan generik harus dimungkinkan, inilah yang saya pikirkan. Dan len() di dunia seperti yang saya lihat akan didefinisikan sebagai: func len(slice []a) , yang merupakan argumen irisan umum yang berarti bahwa ia dapat menghitung panjang dengan cara non-refleksi untuk irisan apa pun. Ini adalah inti dari proposal ini seperti yang saya katakan di atas (manipulasi irisan mudah) dan saya minta maaf saya tidak dapat menyampaikannya dengan baik melalui contoh yang saya berikan dan intisari yang saya buat. Slice generik harus dapat digunakan semudah []int hari ini, saya akan mengatakan lagi bahwa setiap proposal yang tidak membahas ini (slice/array swap, penugasan, len, cap, dll. ) gagal menurut saya.

Semua yang dikatakan, sekarang kami benar-benar jelas tentang apa tujuan masing-masing. Ketika saya mengusulkan apa yang saya lakukan, saya sangat mengatakan bahwa itu hanyalah proposal sintaksis dan detailnya sangat kabur. Tapi kami tetap masuk ke detailnya dan salah satu detail itu akhirnya menjadi kurangnya batasan, ketika saya menulisnya, saya tidak memikirkannya karena itu tidak penting untuk apa yang ingin saya lakukan , bukan berarti kami tidak dapat menambahkannya atau tidak diinginkan. Masalah utama dengan melanjutkan sintaks yang diusulkan dan mencoba untuk mengatasi kendala adalah bahwa definisi argumen generik saat ini berulang (sengaja) sehingga tidak ada referensi ke kode di tempat lain untuk menentukan kendala dll. Jika kita memperkenalkan kendala, saya tidak melihat bagaimana kita bisa menjaga ini.

Contoh tandingan terbaik adalah fungsi sortir yang telah kita diskusikan sebelumnya.

type Sort(slice []a:Lesser, less func(a:Lesser, a:Lesser)) { ... }

Seperti yang Anda lihat, tidak ada cara yang bagus untuk mewujudkannya, dan pendekatan token-spam ke Generics mulai terdengar lebih baik lagi. Untuk mendefinisikan batasan pada ini, kita perlu mengubah dua hal dari proposal asli:

  • Perlu ada cara untuk menunjuk pada argumen tipe dan memberinya batasan.
  • Batasan harus bertahan lebih lama dari satu definisi, mungkin ruang lingkup itu adalah tipe, mungkin ruang lingkup itu adalah file (file sebenarnya terdengar cukup masuk akal).

Penafian: Berikut ini bukan amandemen aktual untuk proposal karena saya hanya membuang simbol acak di luar sana, saya hanya menggunakan sintaks ini sebagai contoh untuk menggambarkan apa yang dapat kami lakukan untuk mengubah proposal seperti aslinya

// Decorator style, follows the definition of the type thorugh all
// of it's methods.
<strong i="14">@a</strong>: Lesser, Hasher, Equaler
func Sort(slice []a) { ... }
<strong i="15">@k</strong>: Equaler, Hasher
type HashTable k v struct

// Inline, follows the definition of the type through
// all of it's methods.
func [a: Hasher, Equaler] Sort(slice []a) { ... }
type [k: Hasher, Equaler] HashTable k v struct

// File-scope global style, if k appears as a generic argument
// it's constrained by this that appears at the top of the file underneath
// the imports but before any other code.
<strong i="16">@k</strong>: Equaler, Hasher

Sekali lagi perhatikan bahwa tidak satu pun di atas yang benar-benar ingin saya tambahkan ke proposal. Saya hanya menunjukkan konstruksi seperti apa yang bisa kita gunakan untuk memecahkan masalah, dan bagaimana tampilannya agak tidak relevan saat ini.

Pertanyaan yang kemudian perlu kita jawab adalah: Apakah kita masih mendapatkan nilai dari argumen generik yang tersirat? Poin utama dari proposal ini adalah untuk menjaga nuansa bahasa Go-like yang bersih, untuk menjaga hal-hal sederhana, untuk menjaga hal-hal cukup rendah kebisingan dengan menghilangkan token yang berlebihan. Dalam banyak kasus di mana tidak ada kendala yang diperlukan, misalnya fungsi peta atau definisi tipe Hasil, apakah terlihat bagus, apakah terasa seperti Go, apakah itu berguna? Dengan asumsi bahwa kendala juga tersedia dalam beberapa bentuk atau lainnya.

func map(slice []a, mapper func(a) b) {
  for i := range slice {
    slice[i] = mapper(slice[i])
  }
}

type Result a b struct {
  Ok  a
  Err b
}

@aarondl Saya akan mencoba menjelaskan. Alasan Anda memerlukan batasan tipe adalah karena itulah satu-satunya cara Anda dapat memanggil fungsi atau metode pada suatu tipe. pertimbangkan tipe yang tidak dibatasi a tipe apa ini, baik itu bisa berupa string atau Int atau apa pun. Jadi kami tidak dapat memanggil fungsi atau metode apa pun di dalamnya karena kami tidak tahu jenisnya. Kita bisa menggunakan tipe-switch dan refleksi runtime untuk mendapatkan tipenya, dan kemudian memanggil beberapa fungsi atau metode di atasnya, tetapi ini adalah sesuatu yang ingin kita hindari dengan obat generik. Ketika Anda membatasi tipe misalnya a adalah Hewan, kami kemudian dapat memanggil metode apa pun yang ditentukan untuk hewan pada a .

Dalam contoh Anda, ya, Anda dapat memasukkan fungsi mapper, tetapi ini akan mengakibatkan fungsi mengambil banyak argumen, dan pada dasarnya seperti bahasa tanpa antarmuka, hanya fungsi kelas satu. Untuk melewati setiap fungsi yang akan Anda gunakan pada tipe a akan mendapatkan daftar fungsi yang sangat panjang dalam program nyata apa pun, terutama jika Anda menulis terutama kode generik untuk injeksi ketergantungan, yang ingin Anda lakukan untuk meminimalkan kopling.

Misalnya bagaimana jika fungsi yang memanggil peta juga generik? Bagaimana jika fungsi yang memanggil itu generik dll. Bagaimana kita mendefinisikan mapper jika kita belum mengetahui jenis a ?

func m(slice []a) []b {
   mapper := func(x a) b {...}
   return map(slice, mapper)
}

Fungsi apa yang dapat kita panggil x ketika mencoba mendefinisikan mapper ?

@keean Saya mengerti tujuan dan fungsi dari kendala. Saya hanya tidak menghargai mereka setinggi hal-hal sederhana seperti struct wadah generik (bukan wadah generik untuk berbicara) dan irisan generik dan karena itu bahkan tidak memasukkannya ke dalam proposal asli.

Saya sebagian besar masih percaya bahwa antarmuka adalah jawaban yang tepat untuk masalah seperti yang Anda bicarakan di mana Anda melakukan injeksi ketergantungan, yang sepertinya bukan tempat yang tepat untuk obat generik tetapi siapa yang harus saya katakan. Tumpang tindih antara tanggung jawab mereka cukup besar di mata saya, oleh karena itu mengapa @Merovius dan saya harus berdiskusi apakah kami bisa hidup tanpa mereka atau tidak, dan dia cukup membuat saya yakin mereka akan berguna dalam beberapa kasus penggunaan maka saya menjelajahi sedikit tentang apa yang mungkin dapat kami lakukan untuk menambahkan fitur ke proposal yang saya buat.

Adapun contoh Anda, Anda tidak dapat memanggil fungsi di x. Tetapi Anda masih dapat mengoperasikan irisan tersebut seperti halnya irisan lain yang sangat berguna dengan sendirinya. Juga tidak yakin apa fungsi di dalam fungsi itu... mungkin Anda bermaksud menetapkan ke var?

@aarondl
Terima kasih, saya memperbaiki sintaksnya, namun saya pikir artinya masih jelas.

Contoh yang saya berikan di atas menggunakan polimorfisme parametrik dan antarmuka untuk mencapai beberapa tingkat pemrograman generik, namun kurangnya pengiriman ganda selalu akan menempatkan batas pada tingkat umum yang dapat dicapai. Karena itu tampaknya Go tidak akan menyediakan fitur yang saya cari dalam suatu bahasa, itu tidak berarti saya tidak dapat menggunakan Go untuk beberapa tugas, dan ternyata saya sudah melakukannya dan berfungsi dengan baik, bahkan jika saya sudah memilikinya untuk memotong-dan-tempel kode yang benar-benar hanya membutuhkan satu definisi. Saya hanya berharap di masa depan jika kode itu perlu diubah, pengembang dapat menemukan semua contoh yang ditempelkan.

Saya kemudian dalam dua pikiran, apakah genaralitas terbatas mungkin tanpa perubahan besar pada bahasa adalah ide yang bagus, mengingat kerumitan yang akan ditambahkannya. Mungkin Go lebih baik tetap sederhana, dan orang dapat menambahkan makro seperti pra-pemrosesan, atau bahasa lain yang dikompilasi ke Go, untuk menyediakan fitur ini? Di sisi lain, menambahkan polimorfisme parametrik akan menjadi langkah pertama yang baik. Mengizinkan parameter tipe tersebut dibatasi akan menjadi langkah selanjutnya yang baik. Kemudian Anda dapat menambahkan parameter tipe terkait ke antarmuka, dan Anda akan memiliki sesuatu yang cukup umum, tetapi itu mungkin sejauh yang Anda bisa dapatkan tanpa pengiriman ganda. Dengan memisahkan menjadi fitur-fitur kecil yang terpisah, saya kira Anda akan meningkatkan peluang agar mereka diterima?

@keean
Apakah multi-pengiriman yang diperlukan? Sangat sedikit bahasa yang mendukungnya. Bahkan C++ tidak mendukungnya. C# agak mendukungnya melalui dynamic tetapi saya tidak pernah menggunakannya dalam praktik dan kata kunci secara umum sangat jarang dalam kode nyata. Contoh yang saya ingat berurusan dengan sesuatu seperti parsing JSON, bukan menulis obat generik.

Apakah multi-pengiriman yang diperlukan?

IMHO, saya pikir @keean berbicara tentang pengiriman ganda statis yang disediakan oleh kelas tipe/antarmuka.
Ini bahkan disediakan dalam C++ dengan metode overloading (saya tidak tahu untuk C#)

Yang Anda maksud adalah pengiriman ganda dinamis yang cukup rumit dalam bahasa statis tanpa tipe serikat pekerja. Bahasa dinamis menghindari masalah ini dengan menghilangkan pemeriksaan tipe statis (inferensi tipe parsial untuk bahasa dinamis, sama untuk Tipe "Dinamis" C#).

Bisakah suatu tipe disediakan sebagai "hanya" parameter?

func Append(t, t2 type, arr []t, value t2) []t {
    v := t(value) // conversion
    return append(arr, v)
}

var arr []float64
v := 0

arr = Append(float64, int, arr, v)

@Inuart menulis:

Bisakah suatu tipe disediakan sebagai "hanya" parameter?

Dipertanyakan sejauh mana hal ini mungkin atau diinginkan dalam go

Apa yang Anda inginkan dapat dicapai sebagai gantinya jika batasan umum didukung:

func Append(arr []t, value s) []t  requires Convertible<s,t>{
    v := t(value) // conversion
    return append(arr, v)
}

var arr []int64
v := 0.5

arr = Append(arr, v)

Ini juga harus dimungkinkan dengan batasan:

func convert(value s) t requires Convertible<s,t>{
    return t(value);
}

f:float64:=2.0

i:int64=convert(f)

Untuk apa nilainya, bahasa Genus kami mendukung banyak pengiriman. Model untuk batasan dapat menyediakan beberapa implementasi yang dikirim ke.

Saya mengerti bahwa notasi Convertible<s,t> diperlukan untuk keamanan waktu kompilasi, tetapi mungkin dapat diturunkan menjadi pemeriksaan runtime

func Append(t, t2 type, arr []t, value t2) []t {
    v, ok := t(value) // conversion
    if !ok {
        panic(...) // or return an err
    }
    return append(arr, v)
}

var arr []float64
v := 0

arr = Append(float64, int, arr, v)

Tapi ini lebih mirip gula sintaks untuk reflect .

@Inuart intinya adalah kompiler dapat memeriksa tipe yang mengimplementasikan kelas tipe pada waktu kompilasi, sehingga pemeriksaan runtime tidak diperlukan. Manfaatnya adalah kinerja yang lebih baik (disebut abstraksi biaya nol). Jika ini adalah pemeriksaan runtime, Anda juga dapat menggunakan reflect .

@creker

Apakah multi-pengiriman yang diperlukan?

Saya terlalu memikirkan hal ini. Di satu sisi pengiriman ganda (dengan kelas tipe mutli-parameter) tidak berfungsi dengan baik dengan eksistensial, apa yang 'Go' sebut 'nilai antarmuka'.

type Equals<T> interface {eq(right T) bool}
(left I) eq(right I) bool {return left == right}
(left I) eq(right F) bool {return false}
(left F) eq(right I) bool {return false}
(left F) eq(right F) bool {return left == right}

func main() {
    x := []Equals<?>{I{2}, F{4.0}, I{2}, F{4.0}}
}

Kami tidak dapat mendefinisikan irisan Equals karena kami tidak memiliki cara untuk menunjukkan bahwa parameter sebelah kanan berasal dari koleksi yang sama. Kami bahkan tidak dapat melakukan ini di Haskell:

data Equals = forall a . IEquals a a => Equals a

Ini tidak baik karena hanya memungkinkan suatu tipe dibandingkan dengan dirinya sendiri

data Equals = forall a b . IEquals a b => Equals a

Ini tidak baik karena kita tidak memiliki cara untuk membatasi b menjadi eksistensial lain dalam koleksi yang sama dengan a (jika a bahkan ada dalam koleksi).

Namun itu membuatnya sangat mudah untuk diperluas dengan tipe baru:

(left K) eq(right I) bool {return false}
(left K) eq(right F) bool {return false}
(left I) eq(right K) bool {return false}
(left F) eq(right K) bool {return false}
(left K) eq(right K) bool {return left == right}

Dan ini akan lebih ringkas dengan instance atau spesialisasi default.

Di sisi lain kita dapat menulis ulang ini di 'Go' yang berfungsi sekarang:

package main

type I struct {v int}
type F struct {v float32}

type EqualsInt interface {eqInt(left I) bool}
func (right I) eqInt (left I) bool {return left == right}
func (right F) eqInt (left I) bool {return false}

type EqualsFloat interface {eqFloat(left F) bool}
func (right I) eqFloat (left F) bool {return false}
func (right F) eqFloat (left F) bool {return left == right}

type EqualsRight interface {
    EqualsInt
    EqualsFloat
}

type EqualsLeft interface {eq(right EqualsRight) bool}
func (left I) eq (right EqualsRight) bool {return right.eqInt(left)}
func (left F) eq (right EqualsRight) bool {return right.eqFloat(left)}

type Equals interface {
    EqualsLeft
    EqualsRight
}

func main() {
    x := []Equals{I{2}, F{4.0}, I{2}, F{4.0}}
    println(x[0].eq(x[1]))
    println(x[1].eq(x[0]))
    println(x[0].eq(x[2]))
    println(x[1].eq(x[3]))
}

Ini bekerja dengan baik dengan eksistensial (nilai antarmuka), namun jauh lebih kompleks, lebih sulit untuk melihat apa yang terjadi dan bagaimana cara kerjanya, dan memiliki batasan besar bahwa kita memerlukan satu antarmuka per jenis dan kita perlu membuat kode keras yang dapat diterima jenis sisi kanan seperti ini:

type EqualsRight interface {
    EqualsInt
    EqualsFloat
}

Yang berarti kita harus memodifikasi sumber pustaka untuk menambahkan tipe baru karena antarmuka EqualsRight tidak dapat diperluas.

Jadi tanpa antarmuka multi-parameter, kami tidak dapat mendefinisikan operator generik yang dapat diperluas seperti kesetaraan. Dengan antarmuka multi-parameter, eksistensial (nilai antarmuka) menjadi bermasalah.

Masalah utama saya dengan banyak sintaks yang diusulkan (sintaks?) Blah[E] adalah bahwa tipe dasarnya tidak menunjukkan informasi apa pun tentang berisi obat generik.

Misalnya:

type Comparer[C] interface {
    Compare(other C) bool
}
// or
type Comparer c interface {
    Compare(other c) bool
}
...

Ini berarti kita mendeklarasikan tipe baru yang menambahkan lebih banyak informasi ke tipe yang mendasarinya. Bukankah maksud dari deklarasi type untuk mendefinisikan nama berdasarkan tipe lain?

Saya akan mengusulkan sintaks lebih di sepanjang baris

type Comparer interface[C] {
    Compare(other C) bool
}

Ini berarti bahwa sebenarnya Comparer hanyalah tipe berdasarkan interface[C] { ... } , dan interface[C] { ... } tentu saja adalah tipe yang terpisah dari interface { ... } . Ini memungkinkan Anda untuk menggunakan antarmuka generik tanpa menamainya, jika Anda mau (yang diizinkan dengan antarmuka normal). Saya pikir solusi ini sedikit lebih intuitif dan bekerja dengan baik dengan sistem tipe Go, meskipun tolong perbaiki saya jika saya salah.

Catatan: Mendeklarasikan tipe generik hanya diperbolehkan pada antarmuka, struct, dan fungsi dengan sintaks berikut:
interface[G] { ... }
struct[G] { ... }
func[G] (vars...) { ... }

Kemudian "menerapkan" obat generik akan memiliki sintaks berikut:
interface[G] { ... }[string]
struct[G] { ... }[string]
func[G] (vars...) { ... }[int](args...)

Dan dengan beberapa contoh untuk membuatnya sedikit lebih jelas:

Antarmuka

package add

type Adder interface[E] {
    // Adds the element and returns the size
    Add(elem E) int
}

// Adds the integer 5 to any implementation of Adder[int].
func AddFiveTo(a Adder[int]) int {
    return a.Add(5)
}

Struktur

package heap

type List struct[T] {
    slice []T
}

func (l *List) Add(elem T) { // T is a type defined by the receiver
    l.slice = append(l.slice, elem)
}

Fungsi

func[A] AddManyTo(a Adder[A], many ...A) {
    for _, each := range a {
        a.Add(each)
    }
}

Ini sebagai tanggapan terhadap draf kontrak Go2 dan saya akan menggunakan sintaksnya, tetapi saya mempostingnya di sini karena ini berlaku untuk proposal apa pun untuk polimorfisme parametrik.

Penyematan parameter tipe tidak boleh diizinkan.

Mempertimbangkan

type X(type T C) struct {
  R // A regular type with method Foo()
  T // Some type parameter
}
// X defines some methods other than Foo(),
// some of which invoke Foo.

untuk beberapa jenis arbitrer R dan beberapa kontrak arbitrer C yang tidak mengandung Foo() .

T akan memiliki semua pemilih yang diperlukan oleh C tetapi instantiasi tertentu T mungkin juga memiliki pemilih lain yang sewenang-wenang, termasuk Foo .

Katakanlah Bar adalah struct, dapat diterima di bawah C , yang memiliki bidang bernama Foo .

X(Bar) bisa menjadi instantiasi ilegal. Tanpa cara untuk menentukan kontrak bahwa suatu tipe tidak memiliki pemilih, ini harus menjadi properti yang disimpulkan.

Metode X(Bar) dapat terus menyelesaikan referensi ke Foo sebagai X(Bar).R.Foo . Hal ini memungkinkan penulisan tipe generik tetapi dapat membingungkan pembaca yang tidak terbiasa dengan nitpickery aturan resolusi. Di luar metode X , pemilih akan tetap ambigu, sementara interface { Foo() } tidak bergantung pada parameter X , beberapa contoh dari X akan tidak memuaskannya.

Melarang penyematan parameter tipe lebih sederhana.

(Namun, jika ini diizinkan, nama bidang akan menjadi T untuk alasan yang sama dengan nama bidang dari S tertanam yang didefinisikan sebagai type S = io.Reader adalah S dan bukan Reader tetapi juga karena tipe yang membuat instance T tidak perlu memiliki nama sama sekali.)

@jimmyfrasche Saya pikir bidang yang disematkan dengan tipe generik cukup berguna sehingga akan baik untuk mengizinkannya, bahkan jika mungkin ada sedikit kecanggungan di beberapa tempat. Saran saya adalah mengasumsikan dalam semua kode generik bahwa tipe yang disematkan telah mendefinisikan semua bidang dan metode yang mungkin di setiap tingkat yang memungkinkan, sehingga dalam kode generik semua metode yang disematkan dan bidang tipe non-generik dihapus.

Jadi diberikan:

type R struct(type T) {
    io.Reader
    T
}

metode pada R tidak akan dapat memanggil Read on R tanpa mengarahkan melalui Reader. Sebagai contoh:

func (r R) Do() {
     r.Read(buf)     // Illegal
     r.Reader.Read(buf)  // ok
}

Satu-satunya sisi buruk yang dapat saya lihat dari ini adalah bahwa tipe dinamis mungkin berisi lebih banyak anggota daripada tipe statis. Sebagai contoh:

func (r R) Do() {
    var x interface{} = r
    x.(io.Reader)    // Succeeds
}

@rogpeppe

Satu-satunya sisi buruk yang dapat saya lihat dari ini adalah bahwa tipe dinamis mungkin berisi lebih banyak anggota daripada tipe statis.

Ini adalah kasus dengan parameter tipe secara langsung, jadi saya pikir itu juga harus baik-baik saja dengan tipe parametrik. Saya pikir solusi untuk masalah yang disajikan @jimmyfrasche mungkin adalah dengan meletakkan set metode yang diinginkan dari tipe parameter dalam kontrak.

contract C(t T) {
  interface { Foo() } (X(T){})
  // ...
}

type X(type T C) struct {
  R // A regular type with method Foo()
  T // Some type parameter
}
// X defines some methods other than Foo(),
// some of which invoke Foo.

Ini akan memungkinkan Foo dipanggil pada X secara langsung. Tentu saja, ini akan bertentangan dengan aturan "tidak ada nama lokal dalam kontrak"...

@stevenblenkinsop Hmm, itu mungkin, jika canggung, untuk melakukannya tanpa mengacu pada X

contract C(t T) {
  struct{ R; T }{}.Foo
}

C masih terikat pada implementasi X meskipun sedikit lebih longgar.

Jika Anda tidak melakukan itu, dan Anda menulis

func (x X(T)) Fooer() interface { Foo() } {
  return x
}

apakah itu mengkompilasi? Itu tidak akan di bawah aturan @rogpeppe yang sepertinya perlu diadopsi juga ketika Anda tidak membuat jaminan dalam kontrak. Tetapi apakah itu hanya berlaku ketika Anda menyematkan argumen tipe tanpa kontrak yang memadai atau untuk semua penyematan?

Akan lebih mudah untuk melarangnya.

Saya mulai mengerjakan proposal ini sebelum draft Go2 diumumkan.

Saya siap untuk menghapus milik saya dengan senang hati ketika saya melihat pengumumannya, tetapi saya masih tidak nyaman dengan kerumitan drafnya, jadi saya menyelesaikan milik saya. Ini kurang kuat tetapi lebih sederhana. Jika tidak ada yang lain, mungkin ada beberapa bagian yang layak untuk dicuri.

Ini memperluas sintaks proposal @ianlancetaylor sebelumnya, karena itulah yang tersedia ketika saya mulai. Itu tidak mendasar. Itu bisa diganti dengan sintaks (type T dll atau sesuatu yang setara. Saya hanya membutuhkan beberapa sintaks sebagai notasi untuk semantik.

Itu terletak di sini: https://Gist.github.com/jimmyfrasche/656f3f47f2496e6b49e041cd8ac716e4

Aturannya adalah bahwa metode apa pun yang dipromosikan dari kedalaman yang lebih besar daripada parameter tipe yang disematkan tidak dapat dipanggil kecuali (1) identitas argumen tipe diketahui atau (2) metode tersebut dinyatakan dapat dipanggil di luar ketik dengan kontrak yang membatasi parameter tipe. Kompiler juga dapat menentukan batas atas dan bawah pada kedalaman yang harus dimiliki metode yang dipromosikan dalam tipe luar O , dan menggunakannya untuk menentukan apakah metode tersebut dapat dipanggil pada tipe yang menyematkan O , yaitu apakah ada potensi konflik dengan metode lain yang dipromosikan atau tidak. Hal serupa juga akan berlaku untuk parameter tipe apa pun yang dinyatakan memiliki metode yang dapat dipanggil, di mana rentang kedalaman metode dalam parameter tipe adalah [0, inf).

Menyematkan parameter jenis sepertinya terlalu berguna untuk melarangnya sepenuhnya. Untuk satu hal, ini memungkinkan komposisi transparan, yang tidak diizinkan oleh pola antarmuka penyematan.

Saya juga menemukan potensi penggunaan dalam mendefinisikan kontrak. Jika Anda ingin dapat menerima nilai tipe T (yang bisa berupa tipe pointer) yang mungkin memiliki metode yang ditentukan pada *T , dan Anda ingin dapat memasukkan nilai itu sebuah antarmuka, Anda tidak perlu memasukkan T di antarmuka, karena metodenya mungkin ada di *T , dan Anda tidak dapat serta-merta memasukkan *T di antarmuka karena T mungkin sendiri merupakan tipe pointer (dan dengan demikian *T mungkin memiliki kumpulan metode kosong). Namun, jika Anda memiliki pembungkus seperti

type Wrapper(type T) { T }

Anda dapat menempatkan *Wrapper(T) di antarmuka dalam semua kasus jika kontrak Anda mengatakan itu memenuhi antarmuka.

Tidak bisakah kamu melakukannya?

type Interface interface {
  SomeMethod(int) error
}

contract MightBeAPointer(t T) {
  Interface(t)
}

func Example(type T MightBeAPointer)(v T) {
  var i Interface = v
  // ...
}

Saya mencoba menangani kasus di mana seseorang menelepon

type S struct{}
func (s *S) SomeMethod(int) error { ... }
...
var s S
Example(S)(s)

Ini tidak akan berhasil karena S tidak dapat dikonversi ke Interface , hanya *S yang dapat.

Jelas, jawabannya mungkin "jangan lakukan itu". Namun, proposal kontrak menjelaskan kontrak seperti:

contract Contract(t T) {
    var _ error = t.SomeMethod(int(0))
}

S akan memenuhi kontrak ini karena pengalamatan otomatis, seperti halnya *S . Apa yang saya coba atasi adalah kesenjangan kemampuan antara pemanggilan metode dan konversi antarmuka dalam kontrak.

Bagaimanapun, ini sedikit bersinggungan, menunjukkan satu potensi penggunaan untuk menyematkan parameter tipe.

Penyematan ulang, saya pikir "dapat menyematkan dalam struct" adalah batasan lain yang harus ditangkap oleh kontrak jika diizinkan.

Mempertimbangkan:

contract Embeddable(type X, Y) {
    type S struct {
        X
        Y
    }
}

type Embedded(type First, Second Embeddable) struct {
        First
        Second
}

// Error: First and Second both provide method Read.
// That must be diagnosed to the Embeddable contract, not the definition of Embedded itself.
type Boom = Embedded(*bytes.Buffer, *strings.Reader)

@bcmills menyematkan tipe dengan penyeleksi yang ambigu diperbolehkan jadi saya tidak yakin bagaimana kontrak itu seharusnya ditafsirkan.

Bagaimanapun, jika Anda hanya menyematkan jenis yang diketahui, tidak apa-apa. Jika Anda hanya menyematkan parameter tipe, tidak apa-apa. Satu-satunya kasus yang menjadi aneh adalah ketika Anda menyematkan satu atau lebih tipe yang diketahui DAN satu atau lebih parameter tipe dan kemudian hanya ketika penyeleksi dari tipe yang diketahui dan argumen tipe tidak terputus

@bcmills menyematkan tipe dengan penyeleksi yang ambigu diperbolehkan jadi saya tidak yakin bagaimana kontrak itu seharusnya ditafsirkan.

Hmm, poin yang bagus. Saya kehilangan satu kendala lagi untuk memicu kesalahan.

contract Embeddable(type X, Y) {
    type S struct {
        X
        Y
    }
    var _ io.Reader = S{}
}

https://play.golang.org/p/3wSg5aRjcQc

Itu membutuhkan salah satu dari X atau Y tetapi tidak keduanya menjadi io.Reader . Sangat menarik bahwa sistem kontrak cukup ekspresif untuk memungkinkan itu. Saya senang saya tidak perlu mencari tahu aturan inferensi tipe untuk binatang seperti itu.

Tapi bukan itu masalahnya.

Saat itulah Anda melakukannya

type S (type T C) struct {
  io.Reader
  T
}
func (s *S(T)) X() io.Reader {
  return s
}

Itu seharusnya gagal dikompilasi karena T dapat memiliki pemilih Read kecuali C memiliki

struct{ io.Reader; T }.Read

Tapi lalu apa aturannya ketika C tidak memastikan set pemilih terputus-putus dan S tidak mereferensikan pemilih? Apakah mungkin untuk setiap instantiasi S untuk memenuhi antarmuka kecuali untuk tipe yang membuat pemilih ambigu?

Apakah mungkin untuk setiap instantiasi S untuk memenuhi antarmuka kecuali untuk tipe yang membuat pemilih ambigu?

Ya, sepertinya memang begitu. Saya ingin tahu apakah itu menyiratkan sesuatu yang lebih dalam ...

Saya belum dapat membangun sesuatu yang sangat buruk, tetapi asimetrinya cukup tidak menyenangkan dan membuat saya merasa tidak nyaman:

type I interface { /* ... */ }
a := G(A) // ok, A satisfies contract
var _ I = a // ok, no selector overlap
b := G(B) // ok, B satisfies contract
var _ = b // error, selector overlap

Saya khawatir tentang pesan kesalahan ketika G0(B) menggunakan G1(B) menggunakan . . . menggunakan Gn(B) dan Gn adalah salah satu yang menyebabkan kesalahan. . . .

FTR, Anda tidak perlu melalui masalah pemilih ambigu untuk memicu kesalahan tipe dengan penyematan.

// Error: Duplicate field name Reader
type Boom = Embedded(*bytes.Reader, *strings.Reader)

Anda berasumsi bahwa nama bidang yang disematkan didasarkan pada tipe argumen, sedangkan kemungkinan besar itu adalah nama dari parameter tipe yang disematkan. Ini seperti saat Anda menyematkan alias tipe dan nama bidangnya adalah alias, bukan nama alias tipenya.

Ini sebenarnya ditentukan dalam desain draf di bagian tentang tipe berparameter :

Ketika tipe berparameter adalah struct, dan parameter tipe disematkan sebagai bidang dalam struct, nama bidang adalah nama parameter tipe, bukan nama argumen tipe.

type Lockable(type T) struct {
    T
    mu sync.Mutex
}

func (l *Lockable(T)) Get() T {
    l.mu.Lock()
    defer l.mu.Unlock()
    return l.T
}

(Catatan: ini bekerja dengan buruk jika Anda menulis Lockable(X) dalam deklarasi metode: haruskah metode mengembalikan lT atau lX? Mungkin kita sebaiknya melarang penyematan parameter tipe dalam sebuah struct.)

Saya hanya duduk kembali di sini di sela-sela dan mengamati. Tapi juga menjadi sedikit khawatir.

Satu hal yang saya tidak malu untuk mengatakan adalah bahwa 90% dari diskusi ini di atas kepala saya.

Tampaknya 20 tahun mencari nafkah dari menulis perangkat lunak tanpa mengetahui apa itu generik, atau polimorfisme parametrik, tidak menghentikan saya untuk menyelesaikan pekerjaan.

Sayangnya saya hanya meluangkan waktu sekitar setahun yang lalu untuk belajar Go. Saya membuat asumsi yang salah bahwa itu adalah kurva belajar yang curam, dan akan memakan waktu terlalu lama untuk menjadi produktif.

Saya tidak mungkin lebih salah.

Saya cukup belajar Go untuk membangun layanan mikro yang benar-benar menghancurkan layanan node.js yang mengalami masalah kinerja dalam waktu kurang dari seminggu.

Ironisnya, saya hanya bermain-main. Saya tidak terlalu serius untuk menaklukkan dunia dengan Go.

Namun, dalam beberapa jam, saya mendapati diri saya duduk dari postur saya yang bungkuk, seperti berada di tepi kursi saya menonton film thriller aksi. API yang saya bangun muncul begitu cepat. Saya menyadari bahwa ini memang bahasa yang layak untuk menginvestasikan waktu berharga saya, karena itu jelas sangat pragmatis dalam desainnya.

Dan itulah hal yang saya sukai dari Go. Ini sangat cepat..... Untuk belajar. Kita semua di sini tahu kemampuan kinerjanya. Tetapi kecepatan mempelajarinya tidak tertandingi oleh 8 bahasa lain yang telah saya pelajari selama bertahun-tahun.

Sejak itu saya menyanyikan pujian Go, dan membuat 4 Dev lagi jatuh cinta padanya. Saya hanya duduk bersama mereka selama beberapa jam dan membangun sesuatu. Hasil berbicara sendiri.

Kesederhanaan, dan kecepatan untuk belajar. Ini adalah fitur pembunuh sebenarnya dari bahasa tersebut.

Bahasa pemrograman yang membutuhkan pembelajaran keras selama berbulan-bulan sering kali tidak mempertahankan pengembang yang ingin mereka tarik. Kami memiliki pekerjaan yang harus dilakukan, dan majikan yang ingin melihat kemajuan setiap hari (terima kasih tangkas, hargai itu)

Jadi, ada dua hal yang saya harap dapat dipertimbangkan oleh tim Go:

1) Masalah sehari-hari apa yang ingin kita pecahkan?

Sepertinya saya tidak dapat menemukan contoh dunia nyata, dengan penghenti acara yang akan diselesaikan oleh obat generik, atau apa pun namanya.

Contoh gaya buku masak dari tugas sehari-hari yang bermasalah, dengan contoh bagaimana tugas tersebut dapat ditingkatkan dengan proposal perubahan bahasa ini.

2) Tetap sederhana, seperti semua fitur hebat lainnya dari Go

Ada beberapa komentar yang sangat cerdas di sini. Tetapi saya yakin bahwa sebagian besar pengembang yang menggunakan Go sehari-hari untuk pemrograman umum seperti saya, sangat senang dan produktif dengan hal-hal sebagaimana adanya.

Mungkin argumen kompiler untuk mengaktifkan fitur-fitur canggih seperti itu? '--keras'

Saya akan sangat sedih jika kami berdampak negatif pada kinerja kompiler. Katakan saja

Dan itulah hal yang saya sukai dari Go. Ini sangat cepat..... Untuk belajar. Kita semua di sini tahu kemampuan kinerjanya. Tetapi kecepatan mempelajarinya tidak tertandingi oleh 8 bahasa lain yang telah saya pelajari selama bertahun-tahun.

Saya sangat setuju. Kombinasi kekuatan dengan kesederhanaan dalam bahasa yang dikompilasi sepenuhnya adalah sesuatu yang benar-benar unik. Saya jelas tidak ingin Go kehilangan itu, dan sebanyak saya menginginkan obat generik, saya pikir mereka tidak layak dengan biaya itu. Saya tidak berpikir itu perlu untuk kehilangan itu, meskipun.

Sepertinya saya tidak dapat menemukan contoh dunia nyata, dengan penghenti acara yang akan diselesaikan oleh obat generik, atau apa pun namanya.

Saya memiliki dua kasus penggunaan utama untuk obat generik: Penghapusan boilerplate tipe-safe dari struktur data yang kompleks, seperti pohon biner, set, dan sync.Map , dan kemampuan untuk menulis _compile-time_ type-safe fungsi yang beroperasi berdasarkan murni pada fungsionalitas argumen mereka, daripada tata letak mereka dalam memori. Ada beberapa hal yang lebih menarik yang saya tidak keberatan bisa melakukannya, tapi saya tidak keberatan _not_ bisa melakukannya jika tidak mungkin menambahkan dukungan untuk mereka tanpa sepenuhnya melanggar kesederhanaan bahasa.

Sejujurnya, sudah ada fitur dalam bahasa yang cukup kasar. Alasan utama mengapa mereka _tidak_ sering disalahgunakan, menurut saya, adalah budaya Go menulis kode 'idiomatik', dikombinasikan dengan perpustakaan standar yang menyediakan contoh kode yang bersih dan mudah ditemukan, untuk sebagian besar. Mendapatkan penggunaan obat generik yang baik ke dalam pustaka standar harus menjadi prioritas saat diterapkan.

@camstuart

Sepertinya saya tidak dapat menemukan contoh dunia nyata, dengan penghenti acara yang akan diselesaikan oleh obat generik, atau apa pun namanya.

Generik sehingga Anda tidak perlu menulis kode sendiri. Jadi Anda tidak perlu lagi mengimplementasikan daftar tertaut, pohon biner, deque, atau antrian prioritas lagi. Anda tidak perlu mengimplementasikan algoritme pengurutan, algoritme partisi, atau algoritme rotasi, dll. Struktur data menjadi penyusun koleksi standar (Peta Daftar misalnya), dan pemrosesan menjadi algoritme standar penyusunan (saya perlu mengurutkan data, partisi, dan memutar). Jika Anda dapat menggunakan kembali komponen-komponen ini, tingkat kesalahan akan turun, karena setiap kali Anda mengimplementasikan kembali Antrian Prioritas, atau algoritme partisi, ada kemungkinan Anda salah dan menimbulkan bug.

Generik berarti Anda menulis lebih sedikit kode, dan menggunakan kembali lebih banyak. Artinya, fungsi perpustakaan standar yang terpelihara dengan baik dan tipe data abstrak dapat digunakan dalam lebih banyak situasi, jadi Anda tidak perlu menulis sendiri.

Lebih baik lagi, semua itu secara teknis dapat dilakukan di Go sekarang, tetapi hanya dengan kehilangan hampir total waktu kompilasi-keamanan tipe _and_ dengan beberapa, berpotensi besar, overhead runtime. Obat generik memungkinkan Anda melakukannya tanpa salah satu dari kerugian tersebut.

Implementasi fungsi umum:

/*

* "generic" is a KIND of types, just like "struct", "map", "interface", etc...
* "T" is a generic type (a type of kind generic).
* var t = T{int} is a value of type T, values of generic types looks like a "normal" type

*/

type T generic {
    int
    float64
    string
}

func Sum(a, b T{}) T{} {
    return a + b
}

Pemanggil fungsi:

Sum(1, 1) // 2
// same as:
Sum(T{int}(1), T{int}(1)) // 2

Implementasi struktur umum:

type ItemT generic {
    interface{}
}

type List struct {
    l []ItemT{}
}

func NewList(t ItemT) *List {
    l := make([]t)
    return &List{l}
}

func (p *List) Push(item ItemT{}) {
    p.l = append(p.l, item)
}

Penelepon:

list := NewList(ItemT{int})
list.Push(42)

Sebagai seseorang yang baru belajar Swift dan tidak menyukainya, tetapi dengan banyak pengalaman dalam bahasa lain seperti Go, C, Java, dll; Saya benar-benar percaya bahwa obat generik (atau templating, atau apa pun yang Anda ingin menyebutnya) bukanlah hal yang baik untuk ditambahkan ke bahasa Go.

Mungkin saya hanya lebih berpengalaman dengan versi Go saat ini, tetapi bagi saya ini terasa seperti regresi ke C++ karena lebih sulit untuk memahami kode yang ditulis orang lain. Placeholder T klasik untuk tipe membuatnya sangat sulit untuk memahami apa yang coba dilakukan oleh suatu fungsi.

Saya tahu ini adalah permintaan fitur yang populer sehingga saya dapat menanganinya jika itu mendarat, tetapi saya ingin menambahkan 2 sen saya (pendapat).

@jlubawy
Apakah Anda tahu cara lain agar saya tidak perlu mengimplementasikan daftar tertaut atau algoritme quicksort? Seperti yang ditunjukkan Alexander Stepanov, sebagian besar pemrogram tidak dapat dengan benar mendefinisikan fungsi "min" dan "maks" jadi apa harapan kita untuk mengimplementasikan algoritma yang lebih kompleks dengan benar tanpa banyak waktu debugging. Saya lebih suka menarik versi standar dari algoritma ini dari perpustakaan dan hanya berlaku untuk tipe yang saya miliki. Apa alternatif yang ada?

@jlubawy

atau templating, atau apa pun yang Anda ingin menyebutnya

Semuanya tergantung pada implementasinya. jika kita berbicara tentang template C++ maka ya, mereka sulit untuk dipahami secara umum. Bahkan menulisnya pun sulit. Di sisi lain, jika kita menggunakan obat generik C# maka itu sama sekali berbeda. Konsepnya sendiri tidak menjadi masalah di sini.

Jika Anda belum tahu, Tim Go telah mengumumkan draf Go 2.0:
https://golang.org/s/go2designs

Ada draf untuk desain Generik di Go 2.0 (kontrak). Anda mungkin ingin melihat dan memberikan umpan balik tentang Wiki mereka.

Ini bagian yang relevan:

Obat generik

Setelah membaca draft, saya bertanya:

Mengapa

T: Dapat ditambahkan

berarti "tipe T yang mengimplementasikan kontrak Addable"? Mengapa menambahkan yang baru?
konsep ketika kita sudah memiliki INTERFACES untuk itu? Tugas antarmuka adalah
memeriksa waktu pembuatan, jadi kami sudah memiliki sarana untuk tidak membutuhkannya
konsep tambahan di sini. Kita dapat menggunakan istilah ini untuk mengatakan sesuatu seperti: Any
ketik T mengimplementasikan antarmuka Addable. Selain itu, T:_ atau T:Any
(menjadi kata kunci khusus apa pun atau alias antarmuka bawaan{}) akan melakukannya
Trik-nya.

Hanya saja saya tidak tahu mengapa menerapkan kembali sebagian besar hal-hal seperti itu. Tidak ada
akal dan AKAN menjadi berlebihan (karena berlebihan adalah penanganan baru kesalahan wrt
penanganan panik).

14-09-2018 6:15 GMT-05:00 Koala Yeung [email protected] :

Jika Anda belum tahu, Tim Go telah mengumumkan draf Go 2.0:
https://golang.org/s/go2designs

Ada draf untuk desain Generik di Go 2.0 (kontrak). Anda mungkin ingin
untuk melihat dan memberikan umpan balik
https://github.com/golang/go/wiki/Go2GenericsFeedback di Wiki mereka
https://github.com/golang/go/wiki/Go2GenericsFeedback .

Ini bagian yang relevan:

Obat generik


Anda menerima ini karena Anda berkomentar.
Balas email ini secara langsung, lihat di GitHub
https://github.com/golang/go/issues/15292#issuecomment-421326634 , atau bisukan
benang
https://github.com/notifications/unsubscribe-auth/AlhWhS8xmN5Y85_aUKT5VnutoOKUAaLLks5ua4_agaJpZM4IG-xv
.

--
Ini adalah tes untuk tanda tangan email yang akan digunakan di TripleMint

Sunting: "[...] akan melakukan trik JIKA ANDA TIDAK MEMBUTUHKAN PERSYARATAN TERTENTU
JENIS ARGUMEN".

17-09-2018 11:10 GMT-05:00 Luis Masuelli [email protected] :

Setelah membaca draft, saya bertanya:

Mengapa

T: Dapat ditambahkan

berarti "tipe T yang mengimplementasikan kontrak Addable"? Mengapa menambahkan yang baru?
konsep ketika kita sudah memiliki INTERFACES untuk itu? Tugas antarmuka adalah
memeriksa waktu pembuatan, jadi kami sudah memiliki sarana untuk tidak membutuhkannya
konsep tambahan di sini. Kita dapat menggunakan istilah ini untuk mengatakan sesuatu seperti: Any
ketik T mengimplementasikan antarmuka Addable. Selain itu, T:_ atau T:Any
(menjadi kata kunci khusus apa pun atau alias antarmuka bawaan{}) akan melakukannya
Trik-nya.

Hanya saja saya tidak tahu mengapa menerapkan kembali sebagian besar hal-hal seperti itu. Tidak ada
akal dan AKAN menjadi berlebihan (karena berlebihan adalah penanganan baru kesalahan wrt
penanganan panik).

14-09-2018 6:15 GMT-05:00 Koala Yeung [email protected] :

Jika Anda belum tahu, Tim Go telah mengumumkan draf Go 2.0:
https://golang.org/s/go2designs

Ada draf untuk desain Generik di Go 2.0 (kontrak). Kamu boleh
mau lihat dan kasih feedback
https://github.com/golang/go/wiki/Go2GenericsFeedback di Wiki mereka
https://github.com/golang/go/wiki/Go2GenericsFeedback .

Ini bagian yang relevan:

Obat generik


Anda menerima ini karena Anda berkomentar.
Balas email ini secara langsung, lihat di GitHub
https://github.com/golang/go/issues/15292#issuecomment-421326634 , atau bisukan
benang
https://github.com/notifications/unsubscribe-auth/AlhWhS8xmN5Y85_aUKT5VnutoOKUAaLLks5ua4_agaJpZM4IG-xv
.

--
Ini adalah tes untuk tanda tangan email yang akan digunakan di TripleMint

--
Ini adalah tes untuk tanda tangan email yang akan digunakan di TripleMint

@luismasuelli-jobsity Jika saya membaca sejarah implementasi generik di Go dengan benar, maka sepertinya alasan untuk memperkenalkan Kontrak adalah karena mereka tidak ingin operator kelebihan beban di Antarmuka.

Proposal sebelumnya yang akhirnya ditolak menggunakan antarmuka untuk membatasi polimorfisme parametrik, tetapi tampaknya telah ditolak karena Anda tidak dapat menggunakan operator umum seperti '+' dalam fungsi tersebut karena tidak dapat ditentukan dalam antarmuka. Kontrak memungkinkan Anda untuk menulis t == t atau t + t sehingga Anda dapat menunjukkan jenis harus mendukung kesetaraan atau penambahan dll.

Sunting: Juga Go tidak mendukung beberapa antarmuka parameter tipe, jadi Go telah memisahkan kelas tipe menjadi dua hal yang terpisah, Kontrak yang menghubungkan parameter tipe fungsi satu sama lain, dan antarmuka yang menyediakan metode. Apa yang hilang adalah kemampuan untuk memilih implementasi kelas tipe berdasarkan beberapa tipe. Bisa dibilang lebih sederhana jika Anda hanya perlu menggunakan antarmuka atau kontrak, tetapi lebih rumit jika Anda perlu menggunakan keduanya secara bersamaan.

Mengapa T:Addable berarti "tipe T yang mengimplementasikan kontrak Dapat Ditambahkan"?

Sebenarnya bukan itu artinya; itu hanya terlihat seperti itu untuk satu jenis argumen. Di bagian lain dalam draf itu membuat komentar bahwa Anda hanya dapat memiliki satu kontrak per fungsi, dan di sinilah perbedaan utama masuk. Kontrak sebenarnya adalah pernyataan tentang jenis fungsi, bukan hanya jenis secara independen. Misalnya, jika Anda memiliki

func Example(type K, V someContract)(k K, v V) V

Anda dapat melakukan sesuatu seperti

contract someContract(k K, v V) {
  k.someMethod(v)
}

Ini sangat menyederhanakan koordinasi beberapa jenis tanpa harus secara berlebihan menentukan jenis dalam tanda tangan fungsi. Ingat, mereka mencoba menghindari 'pola umum yang berulang secara aneh'. Misalnya, fungsi yang sama dengan antarmuka berparameter yang digunakan untuk membatasi tipe akan menjadi seperti

type someMethoder(V) interface {
  someMethod(V)
}

func Example(type K: someMethoder(V), V)(k K, v V) V

Ini agak canggung. Sintaks kontrak memungkinkan Anda untuk tetap melakukan ini jika perlu, karena 'argumen' kontrak diisi secara otomatis oleh kompiler jika kontrak memiliki jumlah yang sama dengan fungsi yang mengetik parameter. Anda dapat menentukannya secara manual jika Anda mau, artinya Anda _bisa_ melakukan func Example(type K, V someContract(K, V))(k K, v V) V jika Anda benar-benar menginginkannya, meskipun itu tidak terlalu berguna dalam situasi ini.

Salah satu cara untuk memperjelas bahwa kontrak adalah tentang seluruh fungsi, bukan argumen individual, adalah dengan hanya mengaitkannya berdasarkan nama. Sebagai contoh,

contract Example(k K, v V) {
  k.someMethod(v)
}

func Example(type K, V)(k K, v V) V

akan sama seperti di atas. Kelemahannya, bagaimanapun, adalah bahwa kontrak tidak akan dapat digunakan kembali dan Anda kehilangan kemampuan untuk menentukan argumen kontrak secara manual.

Sunting: Untuk menunjukkan lebih lanjut mengapa mereka ingin menyelesaikan pola berulang yang aneh, pertimbangkan masalah jalur terpendek yang terus mereka rujuk. Dengan antarmuka berparameter, definisi akan terlihat seperti

type E(Node) interface {
  Nodes() []Node
}

type N(Edge) interface {
  Edges() (from, to Edge)
}

type Graph(type Node: N(Edge), Edge: E(Node)) struct { ... }
func New(type Node: N(Edge), Edge: E(Node))(nodes []Node) *Graph(Node, Edge) { ... }
func (*Graph(Node, Edge)) ShortestPath(from, to Node) []Edge { ... }

Secara pribadi, saya lebih suka cara kontrak ditentukan untuk fungsi. Saya tidak _terlalu_ tertarik untuk hanya memiliki badan fungsi 'normal' sebagai spesifikasi kontrak yang sebenarnya, tetapi saya pikir banyak masalah potensial dapat diselesaikan dengan memperkenalkan semacam penyederhanaan seperti gofmt yang secara otomatis menyederhanakan kontrak untuk Anda, menghapus bagian asing. Kemudian Anda _bisa_ menyalin badan fungsi ke dalamnya, menyederhanakannya, dan memodifikasinya dari sana. Sayangnya, saya tidak yakin seberapa mungkin ini akan diterapkan.

Beberapa hal masih akan sedikit canggung untuk ditentukan, dan tumpang tindih yang tampak antara kontrak dan antarmuka masih tampak agak aneh.

Saya menemukan versi "CRTP" jauh lebih jelas, lebih eksplisit, dan lebih mudah untuk digunakan (tidak perlu membuat kontrak yang hanya ada untuk mendefinisikan hubungan antara kontrak yang sudah ada sebelumnya melalui serangkaian variabel). Diakui, itu bisa menjadi pengalaman bertahun-tahun dengan gagasan itu.

Klarifikasi. Dengan rancangan rancangan , kontrak dapat diterapkan baik fungsi maupun jenisnya .

"""
Bisa dibilang lebih sederhana jika Anda hanya perlu menggunakan antarmuka atau kontrak, tetapi lebih rumit jika Anda perlu menggunakan keduanya secara bersamaan.
"""

Selama mereka mengizinkan Anda, di dalam kontrak, mereferensikan satu atau lebih antarmuka (bukan hanya operator dan fungsi, sehingga memungkinkan KERING), masalah ini (dan klaim saya) akan terpecahkan. Ada kemungkinan saya salah membaca atau tidak sepenuhnya membaca hal-hal kontrak, dan juga kemungkinan fitur tersebut didukung dan saya tidak menyadarinya. Jika tidak, seharusnya begitu.

Tidak bisakah Anda melakukan hal berikut?

contract Example(t T, v V) {
  t.(interface{
    SomeMethod() V
  })
}

Anda tidak dapat menggunakan antarmuka yang dideklarasikan di tempat lain karena batasan bahwa Anda tidak dapat mereferensikan pengidentifikasi dari paket yang sama dengan kontrak yang dideklarasikan, tetapi Anda dapat melakukan ini. Atau mereka bisa saja menghapus batasan itu; tampaknya agak sewenang-wenang.

@DeedleFake Tidak, karena semua jenis antarmuka dapat ditegaskan dengan tipe (dan kemudian berpotensi panik saat runtime, tetapi kontrak tidak dieksekusi). Tapi Anda bisa menggunakan tugas sebagai gantinya.

t.(someInterface) juga berarti bahwa itu harus antarmuka

Poin bagus. Ups.

Semakin banyak contoh yang saya lihat, semakin rawan kesalahan 'cari tahu dari badan fungsi'.

Ada banyak kasus di mana itu membingungkan bagi seseorang, sintaks yang sama untuk operasi yang berbeda, nuansa implikasi dari konstruksi yang berbeda, dll, tetapi alat akan dapat mengambil itu dan menguranginya ke bentuk normal. Tapi kemudian output dari alat tersebut menjadi sub-bahasa de facto untuk mengekspresikan batasan tipe yang harus kita pelajari dengan hafalan, membuatnya lebih mengejutkan ketika seseorang menyimpang dan menulis kontrak dengan tangan.

Saya juga akan mencatat bahwa

contract I(t T) {
  var i interface { Foo() }
  i = t
  t.(interface{})
}

menyatakan bahwa T harus berupa antarmuka dengan setidaknya Foo() tetapi dapat juga memiliki sejumlah metode tambahan lainnya.

T harus berupa antarmuka dengan setidaknya Foo() tetapi juga dapat memiliki sejumlah metode tambahan lainnya

Apakah itu masalah? Bukankah Anda biasanya ingin membatasi sesuatu sehingga memungkinkan fungsionalitas tertentu tetapi Anda tidak peduli dengan fungsionalitas lainnya? Jika tidak, kontrak seperti

contract Example(t T) {
  t + t
}

tidak mengizinkan pengurangan, misalnya. Tetapi dari sudut pandang apa pun yang saya terapkan, saya tidak peduli apakah suatu tipe memungkinkan pengurangan atau tidak. Jika saya membatasinya agar tidak dapat melakukan pengurangan, maka orang akan secara sewenang-wenang tidak dapat, misalnya, meneruskan apa pun yang berfungsi ke fungsi Sum() atau sesuatu. Itu tampaknya membatasi secara sewenang-wenang.

Tidak, itu tidak masalah sama sekali. Itu hanya properti yang tidak intuitif (bagi saya), tapi mungkin itu karena kopi yang tidak mencukupi.

Wajar untuk mengatakan bahwa deklarasi kontrak saat ini perlu memiliki pesan kompiler yang lebih baik untuk digunakan. Dan aturan untuk kontrak yang valid harus ketat.

Hai
Saya membuat proposal batasan untuk obat generik yang saya posting di utas ini sekitar setahun yang lalu.
Sekarang saya sudah membuat versi 2 . Perubahan utama adalah:

  • Sintaksnya telah disesuaikan dengan yang diusulkan oleh tim-go.
  • Pembatasan oleh bidang telah dihilangkan, yang memungkinkan sedikit penyederhanaan.
  • Paragraf yang dianggap tidak terlalu diperlukan telah dihilangkan.

Saya memikirkan pertanyaan yang menarik (tapi mungkin lebih rinci daripada yang sesuai pada tahap ini dalam desain?) mengenai identitas tipe baru-baru ini:

func Foo() interface{} {
    type S struct {}
    return S{}
}

func Bar(type T)() interface{} {
    type S struct {}
    return S{}
}

func Baz(type T)() interface{} {
    type S struct{t T}
    return S{}
}

func main() {
    fmt.Println(Foo() == Foo()) // 1
    fmt.Println(Bar(int)() == Bar(string)()) // 2
    fmt.Println(Baz(int)() == Baz(string)()) // 3
}
  1. Mencetak true , karena tipe dari nilai yang dikembalikan berasal dari deklarasi tipe yang sama.
  2. Cetakan…?
  3. Mencetak false , saya berasumsi.

yaitu pertanyaannya adalah, kapan dua tipe yang dideklarasikan dalam fungsi generik adalah identik dan kapan tidak. Saya tidak berpikir ini dijelaskan dalam desain ~spec~? Setidaknya saya tidak dapat menemukannya sekarang :)

@merovius saya menganggap kasus tengah seharusnya:

fmt.Println(Bar(int)() == Bar(int)()) // 2

Ini adalah kasus yang menarik, dan itu tergantung pada apakah jenisnya "generatif" atau "aplikatif". Sebenarnya ada varian ML yang mengambil pendekatan berbeda. Tipe aplikatif melihat generik sebagai fungsi tipe, dan karenanya f(int) == f(int). Tipe generatif melihat generik sebagai templat tipe yang membuat tipe 'instance' unik baru setiap kali digunakan, jadi t<int> != t<int>. Ini harus didekati pada tingkat sistem tipe keseluruhan karena memiliki implikasi halus untuk penyatuan, inferensi, dan kesehatan. Untuk perincian lebih lanjut dan contoh dari jenis masalah tersebut, saya sarankan membaca makalah "F-ing modules" Andreas Rossberg: https://people.mpi-sws.org/~rossberg/f-ing/ walaupun makalah tersebut berbicara tentang ML " functors" ini karena ML memisahkan sistem tipenya menjadi dua level, dan functor adalah ML yang setara dengan generik dan hanya tersedia di level modul.

@keean Anda berasumsi salah.

@merovius Ya, kesalahan saya, saya melihat pertanyaannya adalah karena parameter tipe tidak digunakan (tipe hantu).

Dengan tipe generatif, setiap instantiasi akan menghasilkan tipe unik yang berbeda untuk 'S', jadi meskipun parameter tidak digunakan, mereka tidak akan sama.

Dengan tipe aplikatif, 'S' dari setiap instantiasi akan menjadi tipe yang sama, sehingga mereka akan sama.

Akan aneh jika hasil dalam kasus 2 berubah berdasarkan optimasi kompiler. Kedengarannya seperti UB.

Ini 2018 orang, saya tidak percaya saya benar-benar harus mengetik ini seperti pada tahun 1982:

func min(x, y int) int {
jika x < y {
kembali x
}
kembali y
}

func max(x, y int) int {
jika x > y {
kembali x
}
kembali y
}

Maksudku, serius, dudes MIN(INT,INT) INT, bagaimana itu TIDAK dalam bahasa?
Aku marah.

@ dataf3l Jika Anda ingin itu berfungsi seperti yang diharapkan dengan pra-pemesanan, maka:

func min(x, y int) int {
   if x <= y {
      return x
   }
   return y
}

Jadi pasangan (min(x, y), max(x, y)) selalu berbeda dan adalah (x, y) atau (y, x), dan karena itu merupakan jenis dua elemen yang stabil.

Jadi alasan lain mengapa ini harus dalam bahasa atau perpustakaan adalah karena kebanyakan orang salah :-)

Saya berpikir tentang < vs <=, untuk bilangan bulat, saya tidak yakin saya cukup melihat perbedaannya.
Mungkin aku hanya bodoh...

Saya tidak yakin saya cukup melihat perbedaannya.

Tidak ada dalam kasus ini.

@cznic benar dalam kasus ini karena bilangan bulat, namun karena utasnya tentang obat generik, saya berasumsi bahwa komentar perpustakaan adalah tentang memiliki definisi umum min dan maks sehingga pengguna tidak perlu mendeklarasikannya sendiri. Membaca ulang OP, saya dapat melihat mereka hanya menginginkan min dan maks sederhana untuk bilangan bulat, jadi saya buruk, tetapi mereka keluar dari topik meminta fungsi integrasi sederhana di utas tentang obat generik :-)

Generik adalah tambahan penting untuk bahasa ini, terutama mengingat kurangnya struktur data bawaan. Sejauh ini pengalaman saya dengan Go adalah bahasa yang bagus dan mudah dipelajari. Ini memiliki trade off yang besar, yaitu Anda harus mengkodekan hal yang sama berulang-ulang.

Mungkin saya kehilangan sesuatu, tetapi ini sepertinya cacat yang cukup besar dalam bahasa. Intinya, ada beberapa struktur data bawaan, dan setiap kali kita membuat struktur data, kita harus menyalin dan menempelkan kode untuk mendukung setiap T .

Saya tidak yakin bagaimana berkontribusi selain memposting pengamatan saya di sini sebagai 'pengguna'. Saya bukan programmer yang cukup berpengalaman untuk berkontribusi pada desain atau implementasi, jadi saya hanya bisa mengatakan bahwa obat generik akan sangat meningkatkan produktivitas dalam bahasa (selama waktu pembuatan dan perkakas tetap mengagumkan seperti sekarang).

@webern Terima kasih. Lihat https://go.googlesource.com/proposal/+/master/design/go2draft.md .

@ianlancetaylor , setelah memposting, ide yang cukup radikal/unik muncul di kepala saya yang menurut saya akan 'ringan' sejauh menyangkut bahasa dan perkakas. Saya belum membaca tautan Anda sepenuhnya, saya akan melakukannya. Tetapi jika saya ingin mengajukan ide/proposal untuk pemrograman generik dalam format MD, bagaimana caranya?

Terima kasih.

@webern Tulis (kebanyakan orang telah menggunakan Intisari untuk format penurunan harga) dan perbarui wiki di sini https://github.com/golang/go/wiki/Go2GenericsFeedback

Banyak orang lain telah melakukannya.

Saya telah menggabungkan (melawan tip terbaru) dan mengunggah CL implementasi prototipe pra-Gophercon kami dari parser (dan printer) yang mengimplementasikan desain draf kontrak. Jika Anda tertarik untuk mencoba sintaksnya, silakan lihat: https://golang.org/cl/149638 .

Untuk bermain dengannya:

1) Cherry-pilih CL dalam repo yang baru-baru ini:
git fetch https://go.googlesource.com/go refs/changes/38/149638/2 && git cherry-pick FETCH_HEAD

2) Bangun kembali dan instal kompiler:
pergi instal cmd/kompilasi

3) Gunakan kompiler:
pergi alat kompilasi foo.go

Lihat deskripsi CL untuk detailnya. Menikmati!

contract Addable(t T) {
    t + t
}

func Sum(type T Addable)(x []T) T {
    var total T
    for _, v := range x {
        total += v
    }
    return total
}

Desain generik ini, func Sum(type T Addable)(x []T) T , SANGAT SANGAT SANGAT JELEK!!!

Untuk dibandingkan dengan func Sum(type T Addable)(x []T) T , menurut saya func Sum<T: Addable> (x []T) T lebih jelas, dan tidak membebani programmer yang berasal dari bahasa pemrograman lain.

Maksud Anda sintaksnya lebih bertele-tele?
Pasti ada beberapa alasan mengapa itu bukan func Sum(T Addable)(x []T) T .

tanpa kata kunci type tidak akan ada cara untuk membedakan antara fungsi generik dan fungsi yang mengembalikan fungsi lain, yang dipanggil sendiri.

@urandom Itu hanya masalah pada waktu instantiasi dan di sana kami tidak memerlukan kata kunci type , tetapi hanya hidup dengan ambiguitas AIUI.

Masalahnya adalah, bahwa tanpa kata kunci type , func Foo(x T) (y T) dapat diuraikan baik sebagai mendeklarasikan fungsi generik yang mengambil T dan tidak mengembalikan apa pun atau fungsi non-generik yang mengambil T dan mengembalikan T .

fungsi Sum(x []T) T

Saya setuju, saya lebih suka sesuatu di sepanjang garis ini. Mengingat perluasan cakupan linguistik yang diwakili oleh obat generik, saya pikir masuk akal untuk memperkenalkan sintaks ini untuk "menarik perhatian" ke fungsi generik.

Saya juga berpikir ini akan membuat kode sedikit lebih mudah (baca: less Lisp-y) untuk diurai untuk pembaca manusia, serta mengurangi kemungkinan mengenai beberapa ambiguitas penguraian yang tidak jelas lebih jauh (lihat "Penguraian Paling Menjengkelkan" C++, untuk membantu memotivasi banyak kehati-hatian).

Ini 2018 orang, saya tidak percaya saya benar-benar harus mengetik ini seperti pada tahun 1982:

func min(x, y int) int {
jika x < y {
kembali x
}
kembali y
}

func max(x, y int) int {
jika x > y {
kembali x
}
kembali y
}

Maksudku, serius, dudes MIN(INT,INT) INT, bagaimana itu TIDAK dalam bahasa?
Aku marah.

Ada alasan untuk itu.
Jika Anda tidak mengerti, Anda bisa belajar atau pergi.
Pilihanmu.

Saya sangat berharap mereka membuatnya lebih baik.
Tapi sikap "Anda bisa belajar atau pergi", tidak memberikan contoh yang baik untuk diikuti orang lain. itu membaca abrasif yang tidak perlu. Saya rasa komunitas ini bukan tentang @petar-dambovaliev. namun, bukan tugas saya untuk memberi tahu Anda apa yang harus dilakukan, atau bagaimana berperilaku online, itu bukan tempat saya.

Saya tahu ada banyak perasaan kuat tentang obat generik, tetapi harap diingat nilai- nilai Gopher kami . Harap menjaga percakapan tetap hormat dan ramah di semua sisi.

@bcmils terima kasih, Anda membuat komunitas menjadi tempat yang lebih baik.

@katzdm setuju, bahasanya sudah memiliki banyak tanda kurung, hal baru ini terlihat sangat ambigu bagi saya

Mendefinisikan generics tampaknya tak terelakkan memperkenalkan hal-hal seperti type's type , yang membuat Go agak rumit.

Semoga ini tidak terlalu di luar topik, tetapi fitur function overload tampaknya banyak bagi saya.

BTW, saya tahu ada beberapa diskusi tentang kelebihan beban .

@xgfone Setuju, bahwa bahasa tersebut sudah memiliki banyak tanda kurung, membuat kodenya tidak jelas.
func Sum<T: Addable> (x []T) T atau func Sum<type T Addable> (x []T) T lebih baik dan lebih jelas.

Untuk konsistensi (dengan generik bawaan), func Sum[T: Addable] (x []T) T lebih baik daripada func Sum<T: Addable> (x []T) T .

Saya mungkin terpengaruh oleh karya sebelumnya dalam bahasa lain, tetapi Sum<T: Addable> (x []T) T memang tampak lebih berbeda dan mudah dibaca pada pandangan pertama.

Saya juga setuju dengan @katzdm karena lebih baik memperhatikan sesuatu yang baru dalam bahasa tersebut. Ini juga cukup akrab bagi pengembang non-Go yang terjun ke Go.

FWIW, ada kemungkinan sekitar 0% Go akan menggunakan kurung sudut untuk obat generik. Tata bahasa C++ tidak dapat diuraikan karena Anda tidak dapat membedakan a < b > c (rangkaian perbandingan yang sah tetapi tidak berarti) dari pemanggilan umum tanpa memahami jenis a, b, dan c. Bahasa lain menghindari penggunaan tanda kurung sudut untuk obat generik karena alasan ini.

func a < b Addable> (...
Saya kira Anda bisa jika Anda menyadari bahwa setelah func Anda hanya dapat memiliki nama fungsi, ( atau < .

@carlmjohnson saya harap Anda benar

f := sum<int>(10)

Tapi di sini Anda tahu bahwa sum adalah kontrak..

Tata bahasa C++ tidak dapat diuraikan karena Anda tidak dapat membedakan a < b > c (rangkaian perbandingan yang sah tetapi tidak berarti) dari pemanggilan umum tanpa memahami jenis a, b, dan c.

Saya pikir perlu ditunjukkan bahwa sementara Go, tidak seperti C++, melarang ini dalam sistem tipe, karena operator < dan > mengembalikan bool s di Go dan < dan > tidak dapat digunakan dengan bool s, _is_ secara sintaksis legal, jadi ini masih menjadi masalah.

Masalah lain dengan kurung sudut adalah List<List<int>> , di mana >> diberi token sebagai operator shift kanan.

Apa masalah dengan menggunakan [] ? Menurut saya sebagian besar hal di atas diselesaikan dengan menggunakannya:

  • Secara sintaksis, f := sum[int](10) , untuk menggunakan contoh di atas, tidak ambigu karena memiliki sintaks yang sama dengan akses array atau peta, dan kemudian sistem tipe dapat mengetahuinya nanti, sama seperti yang sudah harus dilakukan untuk perbedaan antara akses array dan peta, misalnya. Ini berbeda dengan kasus <> karena satu < legal, menyebabkan ambiguitas, tetapi satu [ tidak.
  • func Example[T](v T) T juga tidak ambigu.
  • ]] bukan tokennya sendiri, jadi masalah itu juga dihindari.

Draf desain menyebutkan ambiguitas dalam deklarasi tipe , seperti dalam type A [T] int , tetapi saya pikir ini dapat diselesaikan dengan relatif mudah dalam beberapa cara berbeda. Misalnya, definisi umum dapat dipindahkan ke kata kunci itu sendiri, bukan nama jenisnya, yaitu:

  • func[T] Example(v T) T
  • type[T] A int

Komplikasi di sini bisa berasal dari penggunaan blok deklarasi tipe, seperti

type (
  A int
)

tapi saya pikir ini cukup langka sehingga pada dasarnya mengatakan bahwa jika Anda membutuhkan obat generik maka Anda tidak dapat menggunakan salah satu blok tersebut.

Saya pikir akan sangat disayangkan untuk menulis

type[T] A []T
var s A[int]

karena tanda kurung siku berpindah dari satu sisi A ke sisi lainnya. Tentu saja itu bisa dilakukan, tetapi kita harus bertujuan untuk lebih baik.

Meskipun demikian, penggunaan kata kunci type dalam sintaks saat ini berarti bahwa kita dapat mengganti tanda kurung dengan tanda kurung siku.

Ini tampaknya tidak berbeda dari tipe array vs. sintaks ekspresi menjadi [N]T vs. arr[i] , dalam hal bagaimana sesuatu dinyatakan tidak cocok dengan cara penggunaannya. Ya, di var arr [N]T , tanda kurung siku berakhir di sisi yang sama dari arr seperti saat menggunakan arr , tetapi kami biasanya memikirkan sintaks dalam hal sintaks tipe vs ekspresi menjadi berlawanan.

Saya memperluas dan meningkatkan beberapa ide lama saya yang belum matang untuk mencoba menyatukan obat generik khusus dan bawaan.

Saya tidak yakin apakah membahas ( vs < vs [ dan penggunaan type adalah bikeshedding atau memang ada masalah dengan sintaks

@ianlancetaylor ... bertanya-tanya apakah umpan balik tersebut memerlukan penyesuaian pada desain yang diusulkan? Umpan balik saya sendiri adalah bahwa banyak yang merasa bahwa antarmuka dan kontrak dapat digabungkan, setidaknya pada awalnya. Tampaknya menjadi pergeseran setelah beberapa saat bahwa kedua konsep itu harus disimpan terpisah. Tapi saya bisa saja salah membaca tren. Akan senang melihat opsi eksperimental dalam rilis tahun ini!

Ya, kami sedang mempertimbangkan perubahan rancangan, termasuk melihat banyak kontra-proposal yang dibuat orang. Tidak ada yang diselesaikan.

Jus untuk menambahkan beberapa laporan pengalaman praktis:
Saya menerapkan obat generik sebagai ekstensi bahasa di penerjemah Go saya https://github.com/cosmos72/gomacro. Menariknya, kedua sintaksis

type[T] Pair struct { First T; Second T }
type Pair[T] struct { First T; Second T }

ternyata menimbulkan banyak ambiguitas dalam parser: parser kedua dapat diuraikan sebagai deklarasi bahwa Pair adalah larik struct T , di mana T adalah bilangan bulat konstan. Ketika Pair digunakan ada juga ambiguitas: Pair[int] juga dapat diuraikan sebagai ekspresi alih-alih tipe: itu bisa mengindeks array/slice/peta bernama Pair dengan ekspresi indeks int (catatan: int dan tipe dasar lainnya BUKAN kata kunci yang dicadangkan di Go), jadi saya harus menggunakan sintaks baru - memang jelek, tetapi berhasil:

template[T] type Pair struct { First T; Second T }
type pairOfInt = Pair#[int]
var p Pair#[int]

dan juga untuk fungsi:

template[T] func Sum(args ...T) T { /*...*/ }
Sum#[int] (1,2,3)

Jadi, sementara secara teori saya setuju bahwa sintaksis adalah masalah yang dangkal, saya harus menunjukkan bahwa:
1) dari satu sisi, sintaks itulah yang akan diekspos oleh programmer Go - jadi itu harus ekspresif, sederhana dan mungkin enak
2) dari sisi lain, pilihan sintaks yang buruk akan memperumit parser, typechecker, dan compiler untuk menyelesaikan ambiguitas yang diperkenalkan

Pair[int] juga dapat diuraikan sebagai ekspresi alih-alih tipe: itu bisa mengindeks array/slice/map bernama Pair dengan ekspresi indeks int

Ini bukan ambiguitas penguraian, hanya ambiguitas semantik (sampai setelah resolusi nama); struktur sintaksisnya sama. Perhatikan bahwa Sum#[int] juga bisa berupa tipe atau ekspresi tergantung pada apa Sum itu. Hal yang sama berlaku untuk (*T) dalam kode yang ada. Selama resolusi nama tidak memengaruhi struktur yang sedang diuraikan, Anda baik-baik saja.

Bandingkan ini dengan masalah dengan <> :

f ( a < b , c < d >> (e) )

Anda bahkan tidak dapat menandai ini, karena >> dapat berupa satu atau dua token. Kemudian, Anda tidak dapat mengetahui apakah ada satu atau dua argumen untuk f ... struktur ekspresi berubah secara signifikan tergantung pada apa yang dilambangkan dengan a .

Bagaimanapun, saya tertarik untuk melihat apa pemikiran saat ini di tim tentang obat generik, khususnya, apakah "kendala-adalah-kode" telah diulang atau ditinggalkan. Saya dapat mengerti keinginan untuk menghindari mendefinisikan bahasa kendala yang berbeda, tetapi ternyata menulis kode yang cukup membatasi jenis yang terlibat memaksa gaya yang tidak wajar, dan Anda juga harus membatasi apa yang sebenarnya dapat disimpulkan oleh kompiler tentang jenis berdasarkan kode karena jika tidak, kesimpulan-kesimpulan ini dapat menjadi rumit secara sewenang-wenang, atau dapat mengandalkan fakta-fakta tentang bahasa yang mungkin berubah di masa depan.

@kosmos72

Mungkin saya salah, tetapi selain apa yang dikatakan oleh @stevenblenkinsop , apakah mungkin ada istilah:

a b

bisa juga menyiratkan bahwa b bukan tipe jika b dikenal sebagai alfanumerik (tanpa operator/tanpa pemisah) dengan tambahan [identifier] opsional dan a bukan kata kunci khusus/alfanumerik khusus (mis. tanpa impor/ paket/jenis/fungsi)?.

Tidak tahu tata bahasa go terlalu banyak.

Dalam beberapa hal, tipe seperti int dan Sum[int] akan tetap diperlakukan sebagai ekspresi:

type (
    nodeList = []*Node  // nodeList and []*Node are identical types
    Polar    = polar    // Polar and polar denote identical types
)

Jika go mengizinkan fungsi infiks, maka memang a type tag akan menjadi ambigu karena type dapat berupa fungsi atau tipe infiks.

Saya perhatikan hari ini bahwa ikhtisar masalah proposal ini mengklaim Swift:

Mendeklarasikan bahwa T memenuhi protokol Equatable membuat penggunaan == di badan fungsi menjadi valid. Equatable tampaknya built-in di Swift, tidak mungkin untuk mendefinisikan sebaliknya.

Ini tampaknya lebih dari sekadar sesuatu yang sangat memengaruhi keputusan yang dibuat tentang topik ini, tetapi jika itu memberi orang yang jauh lebih pintar daripada saya adalah beberapa inspirasi, saya ingin menunjukkan bahwa sebenarnya tidak ada sesuatu yang istimewa. tentang Equatable selain yang telah ditentukan sebelumnya dalam bahasa (terutama agar banyak tipe bawaan lainnya dapat "menyesuaikan"). Sangat mungkin untuk membuat protokol serupa:

protocol Equatable2 {
    static func == (lhs: Self, rhs: Self) -> Bool
}

class uniq: Equatable2 {
    static func == (lhs: uniq, rhs: uniq) -> Bool {
        return false
    }
}

let narf = uniq(), poit = uniq()

func !=<T: Equatable2> (lhs: T, rhs: T) -> Bool {
    return !(lhs == rhs)
}

print(narf != poit)

@sighoya
Saya berbicara tentang ambiguitas sintaks a[b] yang diusulkan untuk obat generik, karena sudah digunakan untuk mengindeks irisan dan peta - bukan tentang a b .

Sementara itu saya telah mempelajari Haskell, dan sementara saya tahu sebelumnya Haskell banyak menggunakan inferensi tipe, ekspresi dan kecanggihan generiknya mengejutkan saya.

Sayangnya ia memiliki skema penamaan yang cukup aneh, sehingga tidak selalu mudah untuk dipahami pada pandangan pertama. Misalnya class sebenarnya merupakan batasan untuk tipe (generik atau tidak). Kelas Eq adalah batasan untuk tipe yang nilainya dapat dibandingkan dengan '==' dan '/=':

class Eq a where
  (==) :: a -> a -> Bool
  (/=) :: a -> a -> Bool

berarti tipe a memenuhi batasan Eq jika "spesialisasi" ada (sebenarnya "instance" dalam bahasa Haskell) dari fungsi infix == dan /= yang menerima dua argumen, masing-masing dengan tipe a dan mengembalikan hasil Bool .

Saat ini saya mencoba untuk mengadaptasi beberapa ide yang ditemukan di obat generik Haskell ke proposal untuk obat generik Go, dan melihat seberapa cocoknya. Saya sangat senang melihat bahwa penyelidikan sedang berlangsung dengan bahasa lain di luar C++ dan Java:

contoh Swift di atas, dan contoh Haskell saya, menunjukkan bahwa kendala pada tipe generik sudah digunakan dalam praktik oleh beberapa bahasa pemrograman, dan bahwa sejumlah pengalaman non-sepele pada berbagai pendekatan untuk generik dan kendala ada dan tersedia di antara programmer ini (dan lainnya) bahasa.

Menurut pendapat saya, sangat berharga untuk mempelajari pengalaman seperti itu sebelum menyelesaikan proposal untuk obat generik Go.

Pemikiran liar: jika bentuk batasan yang Anda inginkan untuk dipenuhi oleh tipe generik ternyata kurang lebih kongruen dengan definisi antarmuka, Anda dapat menggunakan sintaks pernyataan tipe yang sudah biasa kita gunakan:

type Comparer interface {
  Compare(v interface{}) (*int, error)
}
type PriorityQueue<T.(Comparer)> struct {
  things []T
}

Maaf jika ini telah dibahas secara mendalam di tempat lain; Saya belum melihatnya, tetapi saya masih terjebak pada literatur. Saya telah mengabaikannya sebentar karena, yah, saya tidak ingin obat generik di versi Go mana pun. Tetapi gagasan itu tampaknya mendapatkan momentum dan rasa tak terhindarkan di masyarakat luas.

@ jesse-amano Sangat menarik bahwa Anda tidak ingin obat generik di versi Go apa pun. Saya merasa ini sulit untuk dipahami karena sebagai seorang programmer saya sangat tidak suka mengulang-ulang. Setiap kali saya memprogram di 'C', saya mendapati diri saya harus mengimplementasikan hal-hal dasar yang sama seperti Daftar atau Pohon pada beberapa tipe data baru, dan mau tidak mau implementasi saya penuh dengan bug. Dengan obat generik, kami hanya dapat memiliki satu versi algoritme apa pun, dan seluruh komunitas dapat berkontribusi untuk menjadikan satu versi itu yang terbaik. Apa solusi Anda untuk tidak mengulangi diri sendiri?

Mengenai poin lainnya, Go tampaknya memperkenalkan sintaks baru untuk batasan umum karena antarmuka tidak mengizinkan operator yang kelebihan beban (seperti '==' dan '+'). Ada dua cara untuk maju dari ini, tentukan mekanisme baru untuk batasan generik, yang tampaknya akan berjalan dengan Go, atau mengizinkan antarmuka membebani operator yang merupakan cara yang saya sukai.

Saya lebih suka opsi kedua karena itu membuat sintaks bahasa lebih kecil dan lebih sederhana, dan memungkinkan tipe numerik baru dideklarasikan yang dapat menggunakan operator biasa, misalnya bilangan kompleks yang dapat Anda tambahkan dengan '+'. Argumen yang menentang ini tampaknya adalah bahwa orang mungkin menyalahgunakan kelebihan operator untuk membuat '+' melakukan hal-hal aneh, tetapi ini tampaknya bukan argumen bagi saya karena saya sudah dapat menyalahgunakan nama fungsi apa pun, misalnya saya dapat menulis fungsi yang disebut 'cetak ' yang menghapus semua data di hard drive saya dan menghentikan program. Saya ingin kemampuan untuk membatasi kelebihan operator dan fungsi agar sesuai dengan sifat aksiomatik tertentu seperti komutatif atau asosiatif, tetapi jika itu tidak berlaku untuk operator dan fungsi, saya tidak melihat banyak gunanya. Operator hanyalah fungsi infiks, dan fungsi hanyalah operator awalan.

Hal lain yang perlu disebutkan adalah bahwa batasan generik yang mereferensikan beberapa parameter tipe sangat berguna, jika batasan generik parameter tunggal adalah predikat pada tipe, batasan multi-parameter adalah hubungan pada tipe. Antarmuka Go tidak boleh memiliki lebih dari satu parameter tipe, jadi sekali lagi sintaks baru perlu diperkenalkan, atau antarmuka perlu didesain ulang.

Jadi saya setuju dengan Anda, Go tidak dirancang sebagai bahasa generik, dan segala upaya untuk menggunakan generik akan menjadi kurang optimal. Mungkin lebih baik menyimpan Go tanpa generik, dan merancang bahasa baru di sekitar generik dari bawah ke atas untuk menjaga bahasa tetap kecil dengan sintaks sederhana.

@keean Saya tidak memiliki keengganan yang kuat untuk mengulangi diri saya sendiri beberapa kali ketika saya perlu, dan pendekatan Go untuk penanganan kesalahan, penerima metode, dll. umumnya tampaknya melakukan pekerjaan yang baik untuk mencegah sebagian besar bug.

Dalam beberapa kasus selama empat tahun terakhir, saya telah menemukan diri saya dalam situasi di mana algoritma yang kompleks tetapi dapat digeneralisasikan perlu diterapkan ke lebih dari dua struktur data yang kompleks tetapi konsisten, dan dalam semua kasus -- dan saya mengatakan ini dengan semua keseriusan -- Saya menemukan pembuatan kode melalui go:generate sudah lebih dari cukup.

Ketika saya membaca laporan pengalaman, dalam banyak kasus saya pikir go:generate atau alat serupa dapat memecahkan masalah, dan dalam beberapa kasus lain saya merasa mungkin Go1 bukan bahasa yang tepat, dan sesuatu yang lain mungkin telah digunakan sebagai gantinya (mungkin dengan pembungkus plugin jika beberapa kode Go diperlukan untuk menggunakannya). Tapi saya sadar cukup mudah bagi saya untuk berspekulasi apa yang _mungkin_ telah saya lakukan, yang _mungkin_ berhasil; Sejauh ini saya tidak memiliki pengalaman praktis yang membuat saya berharap Go1 memiliki lebih banyak cara untuk mengekspresikan tipe generik, tetapi bisa jadi saya memiliki cara berpikir yang aneh tentang berbagai hal, atau bisa jadi saya hanya sangat beruntung hanya bekerja pada proyek yang tidak terlalu membutuhkan obat generik.

Saya berharap jika Go2 akhirnya mendukung sintaks generik, itu akan memiliki pemetaan yang cukup mudah ke logika yang akan dihasilkan, tanpa kasus tepi aneh yang mungkin timbul dari tinju/unboxing, "reifikasi", rantai pewarisan, dll. bahwa bahasa lain harus khawatir.

@jesse-amano Dalam pengalaman saya, ini bukan hanya beberapa kali, setiap program adalah komposisi dari algoritma yang terkenal. Saya tidak ingat kapan terakhir kali saya menulis algoritme asli, mungkin masalah pengoptimalan kompleks yang membutuhkan pengetahuan domain.

Saat menulis sebuah program, hal pertama yang saya lakukan adalah mencoba dan memecah masalah menjadi potongan-potongan terkenal yang dapat saya buat, parser argumen, beberapa streaming file, tata letak UI berbasis kendala. Ini bukan hanya algoritme kompleks yang membuat kesalahan orang, hampir tidak ada orang yang dapat menulis implementasi "min" dan "maks" yang benar untuk pertama kalinya (Lihat: http://componentsprogramming.com/writing-min-function-part5/ ).

Masalah dengan go:generate adalah pada dasarnya hanya prosesor makro, ia tidak memiliki keamanan tipe, Anda entah bagaimana harus mengetik check and error check kode yang dihasilkan, yang tidak dapat Anda lakukan sampai Anda menjalankan generasi. Pemrograman meta semacam ini sangat sulit untuk di-debug. Saya tidak ingin menulis program untuk menulis program, saya hanya ingin menulis program :-)

Jadi perbedaannya dengan obat generik adalah saya dapat menulis program _direct_ sederhana yang dapat diperiksa kesalahannya dan diketik dengan pemahaman saya tentang artinya, tanpa harus membuat kode, dan men-debug itu dan mengerjakan bug kembali ke generator.

Contoh yang sangat sederhana adalah "swap", saya hanya ingin menukar dua nilai, saya tidak peduli apa itu:

swap<A>(x: *A, y: *A) {
   let tmp = *x
   *x = *y
   *y = tmp
}

Sekarang saya pikir sepele untuk melihat apakah fungsi ini benar, dan sepele untuk melihatnya generik dan dapat diterapkan ke tipe apa pun. Mengapa saya ingin mengetikkan fungsi ini berulang kali untuk setiap jenis pointer ke nilai yang mungkin ingin saya gunakan untuk swap. Tentu saja saya dapat membangun algoritme generik yang lebih besar dari ini seperti pengurutan di tempat. Saya tidak berpikir kode go:generate bahkan untuk algoritma sederhana akan mudah untuk dilihat apakah itu benar.

Saya dapat dengan mudah membuat kesalahan seperti:

let tmp = *x
*y = *x
*x = tmp

mengetik ini dengan tangan setiap kali saya ingin menukar konten dua pointer.

Saya mengerti bahwa cara idiomatis untuk melakukan hal semacam ini di Go adalah dengan menggunakan antarmuka kosong, tetapi ini tidak aman untuk tipe dan lambat. Namun menurut saya Go tidak memiliki fitur yang tepat untuk mendukung pemrograman generik semacam ini secara elegan, dan antarmuka kosong memberikan jalan keluar untuk mengatasi masalah tersebut. Daripada benar-benar mengubah gaya go, tampaknya lebih baik mengembangkan bahasa yang cocok untuk obat generik semacam ini dari awal. Menariknya 'Rust' mendapatkan banyak hal umum dengan benar, tetapi karena menggunakan manajemen memori statis daripada pengumpulan sampah, ia menambahkan banyak kerumitan yang sebenarnya tidak diperlukan untuk sebagian besar pemrograman. Saya pikir antara Haskell, Go dan Rust mungkin ada semua bit yang diperlukan untuk membuat bahasa generik arus utama yang layak, semuanya tercampur.

Untuk informasi: Saat ini saya sedang menulis daftar keinginan di Go generik,

dengan maksud untuk benar-benar mengimplementasikannya di Go interpreter gomacro saya, yang sudah memiliki implementasi generik Go yang berbeda (dimodelkan setelah template C++).

Ini belum lengkap, umpan balik diterima :)

@keean

Saya membaca posting blog yang Anda tautkan tentang fungsi min, dan empat posting yang mengarah ke sana. Saya tidak mengamati bahkan upaya untuk membuat argumen bahwa "hampir tidak ada orang yang bisa menulis implementasi yang benar dari 'min'...". Penulis sebenarnya tampaknya mengakui bahwa implementasi pertama mereka _is_ benar... selama domain dibatasi untuk angka. Ini adalah pengenalan objek dan kelas, dan persyaratan bahwa mereka dibandingkan hanya sepanjang satu dimensi, kecuali jika nilai dalam dimensi itu sama, kecuali ketika— dan seterusnya, yang menciptakan kompleksitas tambahan. Persyaratan tersembunyi yang halus yang terlibat dalam kebutuhan untuk mendefinisikan komparator dan fungsi penyortiran dengan hati-hati pada objek yang kompleks adalah alasan mengapa saya _tidak_ menyukai obat generik sebagai konsep (setidaknya di Go; Java dengan Spring sepertinya sudah menjadi lingkungan yang cukup baik untuk menulis bersama-sama sekelompok perpustakaan dewasa menjadi sebuah aplikasi).

Saya pribadi tidak menemukan kebutuhan untuk tipe-keamanan di generator makro; jika mereka menghasilkan kode yang dapat dibaca ( gofmt membantu mengatur standar untuk ini cukup rendah), maka pemeriksaan kesalahan waktu kompilasi harus cukup. Seharusnya tidak masalah bagi pengguna generator (atau kode yang memintanya) untuk produksi; dalam waktu yang sangat kecil saya telah dipanggil untuk menulis algoritme generik sebagai makro, beberapa unit test (biasanya float, string, dan pointer-to-struct — jika ada tipe hard-code yang seharusnya 'jangan dikodekan dengan keras, salah satu dari ketiganya tidak akan kompatibel dengannya; jika salah satu dari ketiganya tidak dapat digunakan dalam algoritme generik, maka itu bukan algoritme generik) sudah cukup untuk memastikan makro bekerja dengan benar.

swap adalah contoh yang buruk. Maaf, tapi itu. Ini sudah menjadi one-liner di Go, tidak perlu fungsi generik untuk membungkusnya dan tidak ada ruang bagi programmer untuk membuat kesalahan yang tidak jelas.

*y, *x = *x, *y

Juga sudah ada sort di perpustakaan standar . Ini menggunakan antarmuka. Untuk membuat versi khusus untuk tipe Anda, tentukan:

type myslice []mytype
func (s myslice) Len() int { return len(s) }
func (s myslice) Less(i, j int) bool { return s[i].whatWouldAlsoBeNeededInAGenericImpl(s[j]) }
func (s myslice) Swap(i, j int) { s[i], s[j] = s[j], s[i] }

Diakui beberapa byte lebih banyak untuk diketik daripada SortableList<mytype>(myThings).Sort() , tetapi _lot_ kurang padat untuk dibaca, tidak cenderung "gagap" di seluruh aplikasi, dan jika bug muncul, saya tidak mungkin membutuhkan sesuatu yang berat seperti jejak tumpukan untuk menemukan penyebabnya. Pendekatan saat ini memiliki beberapa keuntungan, dan saya khawatir kita akan kehilangannya jika kita terlalu bersandar pada obat generik.

@jesse-amano
Masalah dengan 'min/maks' berlaku bahkan jika Anda tidak memahami perlunya pengurutan yang stabil. Misalnya satu pengembang mengimplementasikan min/max untuk beberapa tipe data dalam satu modul, dan kemudian digunakan dalam semacam atau beberapa algoritma lain oleh anggota tim lain tanpa pemeriksaan asumsi yang tepat, dan mengarah ke bug aneh karena tidak stabil.

Saya pikir pemrograman sebagian besar menyusun algoritma standar, sangat jarang programmer membuat algoritma inovatif baru, jadi min/max dan sort hanyalah contoh. Mengambil lubang dalam contoh spesifik yang saya pilih hanya menunjukkan bahwa saya tidak memilih contoh yang sangat bagus, itu tidak membahas poin sebenarnya. Saya memilih "swap" karena sangat sederhana, dan cepat bagi saya untuk mengetik. Saya bisa memilih banyak yang lain, mengurutkan, memutar, mempartisi, yang merupakan algoritma yang sangat umum. Tidak butuh waktu lama ketika Anda menulis program yang menggunakan koleksi seperti pohon merah/hitam untuk muak karena harus mengulang pohon untuk setiap tipe data berbeda yang Anda inginkan koleksinya, karena Anda menginginkan keamanan tipe, dan antarmuka kosong sedikit lebih baik daripada "void*" di 'C'. Kemudian Anda harus melakukan hal yang sama lagi untuk setiap algoritme yang menggunakan masing-masing pohon ini, seperti pre-order, in-order, iterasi pasca-order, pencarian, dan itu sebelum kita masuk ke hal-hal canggih seperti algoritme jaringan Tarjan (terpisah set, tumpukan, pohon merentang minimum, jalur terpendek, aliran, dll.)

Saya pikir pembuat kode memiliki tempatnya, misalnya menghasilkan validator dari skema json atau pengurai dari definisi tata bahasa, tetapi saya tidak berpikir mereka membuat pengganti yang cocok untuk obat generik. Untuk pemrograman generik, saya ingin dapat menulis algoritme apa pun sekali, dan membuatnya jelas, sederhana, dan langsung.

Bagaimanapun saya setuju dengan Anda tentang 'Go', saya tidak berpikir 'Go' dirancang dari awal untuk menjadi bahasa generik yang baik, dan menambahkan generik sekarang mungkin tidak akan menghasilkan bahasa generik yang baik, dan akan kehilangan beberapa keterusterangan dan kesederhanaan yang sudah dimilikinya. Secara pribadi jika Anda harus meraih pembuat kode (di luar hal-hal seperti menghasilkan validator dari json-schema atau parser dari file tata bahasa) maka Anda mungkin menggunakan bahasa yang salah.

Sunting: Mengenai pengujian obat generik dengan "float" "string" "pointer-to-struct", saya tidak berpikir ada banyak algoritma generik yang bekerja pada serangkaian jenis yang beragam, kecuali mungkin 'swap'. Fungsi 'generik' yang sebenarnya sangat terbatas pada pengocokan dan tidak terlalu sering terjadi. Generik yang dibatasi jauh lebih menarik, di mana tipe generik dibatasi oleh beberapa antarmuka. Seperti yang Anda lihat, dengan contoh pengurutan di tempat dari pustaka standar, Anda dapat membuat beberapa obat generik terbatas berfungsi di 'Go' dalam kasus terbatas. Saya suka cara kerja antarmuka Go, dan Anda dapat melakukan banyak hal dengannya. Saya lebih menyukai obat generik yang dibatasi dengan benar. Saya tidak terlalu suka menambahkan mekanisme batasan kedua seperti yang dilakukan oleh proposal generik saat ini. Bahasa di mana antarmuka secara langsung membatasi tipe akan jauh lebih elegan.

Sangat menarik bahwa sejauh yang saya tahu, satu-satunya alasan kendala baru diperkenalkan adalah karena Go tidak mengizinkan operator untuk didefinisikan dalam antarmuka. Proposal generik sebelumnya memang mengizinkan tipe dibatasi oleh antarmuka, tetapi ditinggalkan karena tidak mengatasi operator seperti '+'.

@keean
Mungkin ada tempat yang lebih baik untuk diskusi yang berlarut-larut. (Mungkin tidak; Saya telah melihat sekeliling dan sepertinya ini adalah _tempat_ untuk mendiskusikan obat generik di Go2.)

Saya tentu mengerti perlunya jenis yang stabil! Saya menduga penulis perpustakaan standar Go1 asli juga memahaminya, karena sort.Stable telah ada di sana sejak rilis publik.

Saya pikir hal yang hebat tentang paket sort perpustakaan standar adalah _tidak_ hanya bekerja pada irisan. Ini tentu saja yang paling sederhana ketika penerima adalah irisan, tetapi yang Anda butuhkan hanyalah cara untuk mengetahui berapa banyak nilai dalam wadah (metode Len() int ), bagaimana membandingkannya (metode Less(int, int) bool ), dan cara menukarnya (tentu saja metode Swap(int, int) ). Anda dapat menerapkan sort.Interface menggunakan saluran! Ini lambat, tentu saja, karena saluran tidak dirancang untuk pengindeksan yang efisien, tetapi dapat dibuktikan benar dengan anggaran waktu pelaksanaan yang besar.

Saya tidak bermaksud mempermasalahkan, tetapi masalah dengan contoh yang buruk adalah... itu buruk. Hal-hal seperti sort dan min hanyalah _not_ poin yang mendukung fitur bahasa berdampak tinggi seperti obat generik. Saya merasa cukup kuat bahwa menyodok lubang dalam contoh-contoh ini _melakukan_ alamat poin yang sebenarnya; _my_ point adalah tidak perlu obat generik ketika solusi yang lebih baik sudah ada dalam bahasa tersebut.

@jesse-amano

solusi yang lebih baik sudah ada dalam bahasa

Yang mana? Saya tidak melihat apa pun yang lebih baik daripada obat generik yang dibatasi tipe-aman. Generator tidak Go, polos dan sederhana. Antarmuka dan refleksi menghasilkan kode yang tidak aman, lambat, dan rawan panik. Solusi ini cukup baik karena tidak ada yang lain. Obat generik akan menyelesaikan masalah dengan boilerplate, konstruksi antarmuka kosong yang tidak aman dan, yang terburuk, menghilangkan banyak penggunaan refleksi yang bahkan lebih rentan terhadap kepanikan runtime. Bahkan proposal paket kesalahan baru mengalami kekurangan obat generik dan API-nya akan sangat diuntungkan darinya. Anda dapat melihat As sebagai contoh - tidak idiomatis, mudah panik, sulit digunakan, memerlukan pemeriksaan dokter hewan untuk digunakan dengan benar. Semua karena Go tidak memiliki semua jenis obat generik.

sort , min dan algoritme generik lainnya adalah contoh yang bagus karena menunjukkan manfaat utama dari generik - komposisi. Mereka memungkinkan membangun perpustakaan yang luas dari rutinitas transformasi generik yang dapat dirantai bersama-sama. Dan yang terpenting, ini akan mudah digunakan, aman, cepat (setidaknya mungkin dengan obat generik), tidak perlu boilerplate, generator, antarmuka{}, refleksi, dan fitur bahasa tidak jelas lainnya yang digunakan hanya karena tidak ada cara lain.

@creker

Yang mana?

Untuk menyortir barang, paket sort . Apa pun yang mengimplementasikan sort.Interface dapat diurutkan (dengan algoritme pilihan Anda yang stabil atau tidak stabil; beberapa versi di tempat disediakan melalui paket sort , tetapi Anda bebas menulis sendiri dengan API serupa atau berbeda). Karena pustaka standar sort.Sort dan sort.Stable keduanya beroperasi pada nilai yang diteruskan melalui daftar argumen, nilai yang Anda dapatkan kembali sama dengan nilai yang Anda mulai — dan oleh karena itu, tentu saja, jenisnya Anda dapatkan kembali sama dengan tipe yang Anda mulai. Ini sangat aman untuk tipe, dan kompiler melakukan semua pekerjaan menyimpulkan apakah tipe Anda mengimplementasikan antarmuka yang diperlukan dan mampu _setidaknya_ optimasi waktu kompilasi sebanyak mungkin dengan fungsi sort<T> gaya generik .

Untuk menukar barang, one-liner x, y = y, x . Sekali lagi, tidak ada pernyataan tipe, gips antarmuka, atau refleksi yang diperlukan. Itu hanya menukar dua nilai. Kompiler dapat dengan mudah memastikan operasi Anda aman untuk tipe.

Tidak ada satu alat khusus yang saya anggap sebagai solusi yang lebih baik daripada obat generik dalam semua kasus, tetapi untuk masalah apa pun yang harus diselesaikan oleh obat generik, saya yakin ada solusi yang lebih baik. Saya mungkin salah di sini; Saya masih terbuka untuk melihat contoh sesuatu yang dapat dilakukan oleh obat generik di mana semua solusi yang ada akan menjadi buruk. Tapi jika saya bisa membuat lubang di dalamnya, maka itu bukan salah satu contohnya.

Saya juga tidak terlalu menyukai paket xerrors , tetapi xerrors.As tidak menurut saya sebagai non-idiomatik; ini adalah API yang sangat mirip dengan json.Unmarshal . Mungkin perlu dokumentasi yang lebih baik dan/atau kode contoh, tetapi tidak masalah.

Tapi tidak, sort dan min , dengan sendirinya, adalah contoh yang sangat buruk. Yang pertama sudah ada di Go dan dapat dikomposisi dengan sempurna, semuanya tanpa memerlukan obat generik. Yang terakhir dalam arti luasnya adalah salah satu keluaran dari sort (yang telah kita selesaikan), dan dalam kasus di mana solusi yang lebih terspesialisasi atau dioptimalkan mungkin diperlukan, Anda tetap akan menulis solusi khusus daripada bersandar pada obat generik. Sekali lagi, tidak ada generator, antarmuka{}, refleksi, atau fitur bahasa "tidak jelas" yang digunakan dalam paket sort pustaka standar. Ada antarmuka yang tidak kosong (yang didefinisikan dengan baik di API sehingga Anda mendapatkan kesalahan waktu kompilasi jika Anda salah menggunakannya, disimpulkan sehingga Anda tidak memerlukan gips, dan diperiksa pada waktu kompilasi sehingga Anda tidak perlu pernyataan). Mungkin ada beberapa boilerplate _jika_ koleksi yang Anda sortir adalah irisan, tetapi jika itu adalah struct (seperti yang mewakili simpul akar dari pohon pencarian biner?), Anda dapat membuatnya memenuhi sort.Interface juga, jadi sebenarnya _lebih_ fleksibel daripada koleksi umum.

@jesse-amano

maksud saya adalah tidak perlu obat generik ketika solusi yang lebih baik sudah ada dalam bahasa

Saya rasa solusi yang lebih baik sebenarnya relatif didasarkan pada bagaimana Anda melihatnya. Jika kami memiliki bahasa yang lebih baik, kami dapat memiliki solusi yang lebih baik, itulah mengapa kami ingin menjadikan bahasa ini lebih baik. Misalnya jika ada generik yang lebih baik, kami dapat memiliki sort yang lebih baik di stdlib kami, setidaknya cara saat ini untuk mengimplementasikan antarmuka pengurutan bukanlah pengalaman pengguna yang baik bagi saya, saya masih harus mengetikkan banyak kode serupa yang saya sangat merasa kita bisa abstrak itu pergi.

@jesse-amano

Saya pikir hal yang hebat tentang paket pengurutan perpustakaan standar adalah tidak hanya bekerja pada irisan.

Saya setuju, saya suka jenis standar.

Yang pertama sudah ada di Go dan dapat dikomposisi dengan sempurna, semuanya tanpa memerlukan obat generik.

Ini adalah dikotomi palsu. Antarmuka di Go sudah merupakan bentuk generik. Mekanismenya bukanlah benda itu sendiri. Lihat di luar sintaks, dan lihat tujuannya, yaitu kemampuan untuk mengekspresikan algoritme apa pun secara umum tanpa batasan. Abstraksi antarmuka 'sort' adalah generik, memungkinkan semua tipe data yang dapat mengimplementasikan metode yang diperlukan untuk diurutkan. Notasinya hanya berbeda. Kita bisa menulis:

f<T>(x: T) requires Sortable(T)

Yang berarti bahwa tipe 'T' harus mengimplementasikan antarmuka 'Sortable'. Di 'Go' ini mungkin ditulis func f(x Sortable) . Jadi setidaknya aplikasi fungsi di Go dapat ditangani secara umum, tetapi ada operasi yang tidak bisa seperti aritmatika, atau dereferencing. Go bekerja dengan cukup baik, karena antarmuka dapat dianggap sebagai predikat tipe, tetapi Go tidak memiliki jawaban untuk relasi pada tipe.

Sangat mudah untuk melihat batasannya dengan Go, pertimbangkan:

func merge(x, y Sortable)

di mana kita akan menggabungkan dua hal yang dapat diurutkan, namun Go tidak membiarkan kita memaksakan bahwa kedua hal ini harus sama. Bandingkan ini dengan:

merge<T>(x: T, y: T) requires Sortable(T)

Di sini kita jelas bahwa kita menggabungkan dua jenis sortable yang sama. 'Go' membuang informasi jenis yang mendasarinya dan hanya memperlakukan apa pun yang "dapat diurutkan" sebagai hal yang sama.

Mari kita coba contoh yang lebih baik: katakanlah saya ingin menulis pohon merah/hitam yang dapat berisi tipe data apa pun, sebagai perpustakaan, sehingga orang lain dapat menggunakannya.

Antarmuka di Go sudah merupakan bentuk generik.

Jika demikian, maka masalah ini dapat ditutup karena sudah diselesaikan, karena pernyataan aslinya adalah:

Masalah ini mengusulkan bahwa Go harus mendukung beberapa bentuk pemrograman generik.

Equivokasi merugikan semua pihak. Antarmuka memang _a_ bentuk pemrograman generik, dan memang _tidak_ harus, dengan sendirinya, memecahkan setiap masalah terakhir yang dapat dipecahkan oleh bentuk lain dari pemrograman generik. Jadi mari kita, untuk kesederhanaan, mengizinkan masalah apa pun yang dapat diselesaikan dengan alat di luar cakupan proposal/masalah ini untuk dianggap "diselesaikan tanpa obat generik". (Saya percaya sebagian besar masalah yang dapat dipecahkan yang dihadapi di dunia nyata, jika tidak semua, ada di set itu, tetapi ini hanya untuk memastikan kita semua berbicara dalam bahasa yang sama.)

Pertimbangkan: func merge(x, y Sortable)

Tidak jelas bagi saya mengapa menggabungkan dua hal yang dapat diurutkan (atau hal-hal yang mengimplementasikan sort.Interface ) akan berbeda dengan cara menggabungkan dua koleksi _secara umum_. Untuk irisan, itu append ; untuk peta, itu for k, v := range m { n[k] = v } ; dan untuk struktur data yang lebih kompleks, tentu ada strategi penggabungan yang lebih kompleks tergantung pada strukturnya (yang isinya mungkin diperlukan untuk menerapkan beberapa metode yang dibutuhkan struktur). Dengan asumsi Anda berbicara tentang algoritma pengurutan yang lebih rumit yang mempartisi dan memilih sub-algoritma untuk partisi sebelum menggabungkannya kembali, yang Anda butuhkan bukanlah partisi yang "dapat diurutkan" melainkan semacam jaminan bahwa partisi Anda sudah _diurutkan_ sebelum digabungkan. Itu adalah jenis masalah yang sangat berbeda, dan bukan masalah yang sintaks template membantu memecahkan dengan cara yang jelas; tentu saja, Anda ingin beberapa pengujian unit yang cukup ketat untuk menjamin keandalan algoritme pengurutan gabungan Anda, tetapi tentunya Anda tidak ingin mengekspos _exported_ API yang membebani pengembang dengan hal-hal semacam ini.

Anda mengangkat poin menarik tentang Go yang tidak memiliki cara yang baik untuk memeriksa apakah dua nilai memiliki tipe yang sama tanpa refleksi, sakelar tipe, dll. Saya merasa menggunakan interface{} adalah solusi yang dapat diterima dengan sempurna di kasus wadah tujuan umum (misalnya daftar tertaut melingkar) sebagai pelat ketel yang terlibat dalam membungkus API untuk keamanan tipe benar-benar sepele:

type MyStack struct { stack Stack }
func (s *MyStack) Push(v MyType) error { return s.stack.Push(v) }
func (s *MyStack) Pop() (MyType, error) {
  v, err := s.stack.Pop()
  var m MyType
  if v != nil {
    if m, ok := v.(MyType); ok { return m, err; }
    panic("this code should be unreachable from the exported API")
  }
  return nil, err
}

Saya kesulitan membayangkan mengapa boilerplate ini menjadi masalah, tetapi jika ya, alternatif yang masuk akal mungkin adalah templat (teks/). Anda dapat membubuhi keterangan jenis yang ingin Anda definisikan tumpukan dengan komentar //go:generate stackify MyType github.com/me/myproject/mytype , dan biarkan go generate menghasilkan boilerplate untuk Anda. Selama cmd/stackify/stackify_test.go mencobanya dengan setidaknya satu struct dan setidaknya satu tipe bawaan, dan dikompilasi dan lolos, saya tidak mengerti mengapa ini akan menjadi masalah -- dan mungkin cukup dekat dengan apa yang akhirnya dilakukan oleh kompiler mana pun "di bawah tenda" jika Anda mendefinisikan templat. Satu-satunya perbedaan adalah kesalahannya lebih membantu karena kurang padat.

(Mungkin juga ada kasus di mana kita menginginkan _sesuatu_ generik yang lebih peduli pada dua hal dengan tipe yang sama daripada perilakunya, yang tidak termasuk dalam kategori "wadah barang". Itu akan sangat menarik, tapi menambahkan sintaks konstruksi template generik ke bahasa mungkin masih bukan satu-satunya solusi yang tersedia.)

Misalkan boilerplate _bukan_ masalah, saya tertarik untuk mengatasi masalah membuat pohon merah/hitam yang mudah digunakan oleh penelepon sebagai paket seperti sort atau encoding/json . Saya pasti akan gagal karena... yah, saya tidak begitu pintar. Tapi saya senang mengetahui seberapa dekat saya.

Sunting: Awal dari sebuah contoh dapat dilihat di sini , meskipun jauh dari selesai (terbaik yang bisa saya lakukan dalam beberapa jam). Tentu saja, ada juga upaya lain pada struktur data yang serupa.

@jesse-amano

Jika demikian, maka masalah ini dapat ditutup karena sudah > terpecahkan, karena pernyataan aslinya adalah:

Bukan hanya bahwa antarmuka _adalah_ suatu bentuk obat generik, tetapi peningkatan pendekatan antarmuka dapat membuat kita sepenuhnya menggunakan obat generik. Misalnya antarmuka multi-parameter (di mana Anda dapat memiliki lebih dari satu 'penerima') akan memungkinkan hubungan pada tipe. Mengizinkan antarmuka untuk menimpa operator seperti penambahan dan dereferensi akan menghilangkan kebutuhan akan segala bentuk batasan lain pada tipe. Antarmuka _dapat_ menjadi semua jenis batasan yang Anda butuhkan, jika dirancang dengan pemahaman tentang titik akhir dari obat generik yang sepenuhnya umum.

Antarmuka secara semantik mirip dengan kelas tipe Haskell, dan sifat Rust yang _melakukan_ memecahkan masalah umum ini. Kelas tipe dan sifat menyelesaikan semua masalah umum yang sama dengan yang dilakukan templat C++, tetapi dengan cara yang aman untuk tipe (tapi mungkin tidak semua penggunaan pemrograman meta, yang menurut saya adalah hal yang baik).

Saya kesulitan membayangkan mengapa boilerplate ini menjadi masalah, tetapi jika ya, alternatif yang masuk akal mungkin adalah templat (teks/).

Saya pribadi tidak memiliki masalah dengan boilerplate sebanyak itu, tetapi saya memahami keinginan untuk tidak memiliki boilerplate sama sekali, sebagai seorang programmer itu membosankan dan berulang, dan itu adalah jenis tugas yang harus kita hindari untuk menulis program. Jadi sekali lagi, secara pribadi, saya pikir menulis implementasi untuk antarmuka 'tumpukan'/kelas tipe adalah cara yang _right_ untuk membuat tipe data Anda 'dapat ditumpuk'.

Ada dua batasan dengan Go yang menggagalkan pemrograman generik lebih lanjut. Soal ekivalensi 'tipe', misalnya mendefinisikan fungsi matematika sehingga hasil dan semua argumen harus sama. Kita bisa membayangkan:

mul<T>(x, y T) T requires Addable(T) {
    r := 0
    for i := 0; i < y; ++i  {
        r = r + x
    }
    return r
}

Untuk memenuhi batasan pada '+' kita perlu memastikan bahwa x dan y adalah numerik, tetapi juga memiliki tipe dasar yang sama.

Yang lainnya adalah batasan antarmuka hanya untuk satu jenis 'penerima'. Batasan ini berarti Anda tidak hanya harus mengetikkan boilerplate di atas satu kali (yang menurut saya masuk akal) tetapi untuk setiap jenis berbeda yang ingin Anda masukkan ke dalam MyStack. Yang kami inginkan adalah mendeklarasikan tipe yang terkandung sebagai bagian dari antarmuka:

type Stack<T> interface {...}

Ini akan memungkinkan, antara lain, sebuah implementasi untuk dinyatakan parametrik di T sehingga kita dapat menempatkan T di MyStack menggunakan antarmuka Stack, selama semua penggunaan Push dan Munculkan contoh yang sama dari MyStack yang beroperasi pada tipe 'nilai' yang sama.

Dengan dua perubahan ini, kita seharusnya dapat membuat pohon merah/hitam generik. Seharusnya mungkin tanpa mereka, tetapi seperti Stack, Anda harus mendeklarasikan instance baru dari antarmuka untuk setiap jenis yang ingin Anda masukkan ke dalam pohon merah/hitam.

Dari sudut pandang saya, hanya dua ekstensi di atas ke antarmuka yang diperlukan untuk Go untuk sepenuhnya mendukung 'generik'.

@jesse-amano
Melihat contoh pohon merah/hitam, yang sebenarnya kita inginkan secara umum adalah definisi 'Peta' pohon merah/hitam hanyalah satu kemungkinan implementasi. Karena itu, kita mungkin mengharapkan antarmuka seperti ini:

type Map<Key, Value> interface {
   put(x Key, y Value) 
   get(x Key) Value
}

Kemudian pohon merah/hitam dapat disediakan sebagai implementasi. Idealnya kami ingin menulis kode yang tidak bergantung pada implementasi, sehingga Anda dapat menyediakan tabel hash, atau pohon merah-hitam, atau BTree. Kami kemudian akan menulis kode kami:

f<K, V, T>(index T) T requires Map<K, V> {
   ...
}

Sekarang apa pun f itu, ia dapat bekerja secara independen dari implementasi Peta, f mungkin merupakan fungsi perpustakaan yang ditulis oleh orang lain, yang tidak perlu tahu apakah aplikasi saya menggunakan merah/ pohon hitam atau peta hash.

Dalam perjalanan seperti sekarang, kita perlu mendefinisikan peta tertentu seperti ini:

type MapIntString interface {
   put(x Int, y String)
   get(x Int) String
}

Itu tidak terlalu buruk, tetapi itu berarti fungsi 'perpustakaan' f harus ditulis untuk setiap kemungkinan kombinasi tipe kunci dan nilai jika kita ingin dapat menggunakannya dalam aplikasi di mana kita tidak' t tahu jenis kunci dan nilai saat kita menulis perpustakaan.

Sementara saya setuju dengan komentar terakhir @keean , kesulitannya adalah menulis pohon merah/hitam di Go yang mengimplementasikan antarmuka yang dikenal, seperti yang baru saja disarankan.

Tanpa obat generik, diketahui bahwa untuk mengimplementasikan wadah agnostik tipe, seseorang harus menggunakan interface{} dan/atau refleksi - sayangnya kedua pendekatan lambat dan rawan kesalahan.

@keean

Bukan hanya bahwa antarmuka adalah bentuk obat generik, tetapi meningkatkan pendekatan antarmuka dapat membuat kita sepenuhnya menggunakan obat generik.

Saya tidak melihat proposal apa pun yang terkait dengan masalah ini, hingga saat ini, sebagai peningkatan. Tampaknya cukup tidak kontroversial untuk mengatakan bahwa mereka semua cacat dalam beberapa hal. Saya yakin kekurangan itu jauh lebih besar daripada manfaat apa pun, dan banyak manfaat _yang diklaim_ sebenarnya sudah didukung oleh fitur yang ada. Keyakinan saya didasarkan pada pengalaman praktis, bukan spekulasi, tetapi masih bersifat anekdot.

Saya pribadi tidak memiliki masalah dengan boilerplate sebanyak itu, tetapi saya memahami keinginan untuk tidak memiliki boilerplate sama sekali, sebagai seorang programmer itu membosankan dan berulang, dan itu adalah jenis tugas yang harus kita hindari untuk menulis program.

Saya juga tidak setuju dengan ini. Sebagai seorang profesional yang dibayar, tujuan saya adalah untuk mengurangi biaya waktu/usaha _untuk diri saya sendiri dan orang lain_, sambil meningkatkan keuntungan majikan saya, bagaimanapun itu mungkin diukur. Sebuah tugas yang "membosankan" hanya buruk jika juga memakan waktu; itu tidak sulit, atau tidak akan membosankan. Jika hanya sedikit memakan waktu di muka, tetapi menghilangkan aktivitas yang memakan waktu di masa depan dan/atau membuat produk dirilis lebih cepat, maka itu masih sangat berharga.

Kemudian pohon merah/hitam dapat disediakan sebagai implementasi.

Saya pikir saya telah membuat kemajuan yang layak dalam beberapa hari terakhir ini pada implementasi pohon merah/hitam, (belum selesai; bahkan tidak memiliki readme) tetapi saya khawatir saya telah gagal untuk menggambarkan poin saya jika tidak banyak jelas bahwa tujuan saya bukan untuk bekerja menuju antarmuka melainkan untuk bekerja menuju implementasi. Saya sedang menulis pohon merah/hitam, dan tentu saja saya ingin pohon itu _berguna_, tetapi saya tidak peduli _spesifik_ apa yang mungkin ingin digunakan oleh pengembang lain.

Saya tahu bahwa antarmuka minimal yang diperlukan oleh perpustakaan pohon merah/hitam adalah di mana ada pemesanan "lemah" pada elemen-elemennya, jadi saya memerlukan sesuatu _like_ fungsi bernama Less(v interface{}) bool , tetapi jika pemanggil memiliki metode yang melakukan sesuatu yang serupa tetapi tidak bernama Less(v interface{}) bool , terserah mereka untuk menulis pembungkus/shim boilerplate untuk membuatnya berfungsi.

Ketika Anda mengakses elemen yang dikandung oleh pohon merah/hitam, Anda mendapatkan interface{} , tetapi jika Anda bersedia memercayai jaminan saya bahwa perpustakaan menyediakan _is_ pohon merah/hitam, saya tidak mengerti mengapa Anda mau' t percaya bahwa jenis elemen yang Anda masukkan akan sama persis dengan jenis elemen yang Anda keluarkan. Jika Anda _do_ memercayai kedua jaminan tersebut, maka perpustakaan tidak rawan kesalahan sama sekali. Cukup tulis (atau tempel) selusin baris kode untuk menutupi pernyataan tipe.

Sekarang Anda memiliki perpustakaan yang sangat aman (sekali lagi, dengan asumsi tidak lebih dari tingkat kepercayaan yang harus Anda berikan untuk mengunduh perpustakaan di tempat pertama) yang bahkan memiliki nama fungsi persis yang Anda inginkan. Ini penting. Dalam ekosistem gaya Java di mana penulis perpustakaan membungkuk ke belakang untuk membuat kode terhadap definisi antarmuka _exact_ (mereka hampir _memiliki_ untuk, karena bahasa memaksanya melalui sintaks class MyClassImpl extends AbstractMyClass implements IMyClass ) dan ada banyak birokrasi tambahan, Anda harus berusaha keras untuk membuat fasad perpustakaan pihak ketiga agar sesuai dengan standar pengkodean organisasi Anda (yang merupakan jumlah boilerplate yang sama, jika tidak lebih), atau biarkan ini menjadi "pengecualian" untuk standar pengkodean organisasi Anda (dan akhirnya organisasi Anda memiliki banyak pengecualian dalam standarnya seperti dalam basis kodenya), atau menyerah menggunakan perpustakaan yang sangat bagus (dengan asumsi, demi argumen, bahwa perpustakaan itu sebenarnya bagus).

Idealnya kami ingin menulis kode yang tidak bergantung pada implementasi, sehingga Anda dapat menyediakan tabel hash, atau pohon merah-hitam, atau BTree.

Saya setuju dengan ideal ini, tapi menurut saya Go sudah memenuhinya. Dengan antarmuka seperti:

type MyStorage interface {
  Get(KeyType) (ValueType, error)
  Put(KeyType, ValueType) error
}

satu-satunya hal yang hilang adalah kemampuan untuk membuat parameter apa itu KeyType dan ValueType , dan saya tidak yakin ini sangat penting.

Sebagai pengelola (hipotetis) dari perpustakaan pohon merah/hitam, saya tidak peduli apa tipe Anda. Saya hanya akan menggunakan interface{} untuk semua fungsi inti saya yang menangani "beberapa data", dan _maybe_ memberikan beberapa contoh fungsi yang diekspor yang memungkinkan Anda menggunakannya lebih mudah dengan tipe umum seperti string dan int . Tetapi terserah kepada penelepon untuk menyediakan lapisan yang sangat tipis di sekitar API ini agar aman untuk jenis kustom apa pun yang mungkin akhirnya mereka definisikan. Tetapi satu-satunya hal penting tentang API yang saya sediakan adalah memungkinkan penelepon untuk melakukan semua hal yang mereka harapkan dapat dilakukan oleh pohon merah/hitam.

Sebagai penelepon (hipotetis) dari perpustakaan pohon merah/hitam, saya mungkin hanya menginginkannya untuk penyimpanan cepat dan waktu pencarian. Saya tidak peduli bahwa itu adalah pohon merah/hitam. Saya peduli bahwa saya dapat Get hal-hal darinya dan Put hal-hal di dalamnya, dan — yang penting — saya peduli apa hal-hal itu. Jika perpustakaan tidak menawarkan fungsi bernama Get dan Put , atau tidak dapat berinteraksi secara sempurna dengan tipe yang telah saya tetapkan, itu tidak masalah bagi saya selama itu mudah bagi saya untuk menulis sendiri metode Get dan Put , dan membuat tipe saya sendiri memenuhi antarmuka yang dibutuhkan perpustakaan saat saya melakukannya. Jika tidak mudah, saya biasanya menemukan bahwa itu adalah kesalahan penulis perpustakaan, bukan bahasanya, tetapi sekali lagi mungkin ada contoh tandingan yang tidak saya sadari.

Omong-omong, kode bisa menjadi lebih kusut jika _tidak_ seperti ini. Seperti yang Anda katakan, ada banyak kemungkinan implementasi dari penyimpanan kunci/nilai. Meneruskan "konsep" kunci/penyimpanan nilai abstrak menyembunyikan kerumitan bagaimana penyimpanan kunci/nilai dilakukan, dan pengembang di tim saya mungkin memilih yang salah untuk tugas mereka (termasuk versi masa depan saya yang pengetahuannya tentang kunci /implementasi penyimpanan nilai telah kehabisan memori!). Aplikasi atau pengujian unitnya mungkin, terlepas dari upaya terbaik kami dalam tinjauan kode, berisi kode yang bergantung pada implementasi halus yang berhenti bekerja dengan andal ketika beberapa penyimpanan kunci/nilai bergantung pada koneksi ke DB dan yang lainnya tidak. Menyedihkan ketika laporan kesalahan disertai dengan jejak tumpukan besar, dan satu-satunya baris dalam jejak tumpukan yang merujuk sesuatu di basis kode _real_ menunjuk pada baris yang menggunakan nilai antarmuka, semua karena implementasi antarmuka itu menghasilkan kode (yang Anda hanya dapat melihat di runtime) alih-alih struct biasa, dengan metode mengembalikan nilai kesalahan yang dapat dibaca.

@jesse-amano
Saya setuju dengan Anda, dan saya suka cara 'Pergi' dalam melakukan hal-hal di mana kode "pengguna" mendeklarasikan antarmuka yang mengabstraksi cara kerjanya, dan kemudian Anda menulis implementasi antarmuka itu untuk perpustakaan/dependensi. Ini mundur dari cara kebanyakan bahasa lain berpikir tentang antarmuka. tapi begitu Anda mendapatkannya sangat kuat.

Saya masih ingin melihat hal-hal berikut dalam bahasa umum:

  • jenis parametrik, seperti: RBTree<Int, String> karena ini akan menegakkan keamanan jenis koleksi pengguna.
  • ketik variabel, seperti: f<T>(x, y T) T , karena ini diperlukan untuk mendefinisikan keluarga fungsi terkait seperti penambahan, pengurangan dll di mana fungsinya polimorfik, tetapi kami mengharuskan semua argumen memiliki tipe dasar yang sama.
  • batasan tipe, seperti: f<T: Addable>(x, y T) T , yang menerapkan antarmuka ke variabel tipe, karena begitu kita memperkenalkan variabel tipe, kita memerlukan cara untuk membatasi variabel tipe tersebut alih-alih memperlakukan Addable sebagai tipe. Jika kita menganggap Addable sebagai sebuah tipe dan menulis f(x, y Addable) Addable , kita tidak memiliki cara untuk mengetahui apakah tipe dasar asli dari x dan y sama dengan satu sama lain atau tipe yang dikembalikan.
  • antarmuka multi-parameter, seperti: type<K, V> Map<K, V> interface {...} , yang dapat digunakan seperti merge<K, V, T: Map<K, V>>(x, y T) T yang memungkinkan kita untuk mendeklarasikan antarmuka yang diparameterisasi tidak hanya oleh jenis wadah, tetapi dalam hal ini juga kunci dan nilai jenis peta.

Saya pikir masing-masing akan meningkatkan kekuatan abstraksi bahasa.

Ada kemajuan atau jadwal dalam hal ini?

@leaxoy Ada jadwal pembicaraan tentang "Generics in Go" oleh @ianlancetaylor di GopherCon . Saya berharap untuk mendengar lebih banyak tentang keadaan saat ini dalam pembicaraan itu.

@griesemer Terima kasih untuk tautan itu.

@keean Saya juga ingin melihat klausa Where dari Rust di sini, yang mungkin merupakan peningkatan pada proposal type constraints Anda. Ini memungkinkan penggunaan sistem tipe untuk membatasi perilaku seperti "memulai transaksi sebelum kueri" untuk diperiksa tipenya tanpa refleksi runtime. Lihat video ini di sini: https://www.youtube.com/watch?v=jSpio0x7024

@jadbox maaf jika penjelasan saya tidak jelas, tetapi klausa 'di mana' hampir persis seperti yang saya usulkan. Hal-hal setelah 'where' in rust adalah batasan tipe, tetapi saya pikir saya menggunakan kata kunci 'requires' di posting sebelumnya. Semua hal ini dilakukan di Haskell setidaknya satu dekade yang lalu, kecuali Haskell menggunakan operator '=>' dalam tanda tangan tipe untuk menunjukkan batasan tipe, tetapi mekanisme dasarnya sama.

Saya meninggalkan ini dari posting ringkasan saya di atas karena saya ingin menjaga semuanya tetap sederhana, tetapi saya ingin sesuatu seperti ini:

merge<K, V, T>(x, y T) T requires T: Map<K, V>

Tapi itu tidak benar-benar menambahkan apa pun pada apa yang dapat Anda lakukan selain dari sintaks yang dapat lebih mudah dibaca untuk rangkaian batasan yang panjang. Anda dapat mewakili apa pun yang Anda bisa dengan klausa 'di mana' dengan meletakkan batasan setelah mereka mengetikkan variabel dalam deklarasi awal seperti ini:

merge<K, V, T: Map<K, V>>(x, y T) T

Asalkan Anda dapat mereferensikan variabel tipe sebelum dideklarasikan, Anda dapat meletakkan batasan apa pun di sana, dan Anda akan menggunakan daftar yang dipisahkan koma untuk menerapkan beberapa batasan ke variabel tipe yang sama.

Sejauh yang saya ketahui, satu-satunya keuntungan dari klausa 'di mana'/'memerlukan' adalah bahwa semua variabel tipe sudah dideklarasikan di depan, yang dapat mempermudah pengurai dan inferensi jenis.

Apakah ini masih thread yang tepat untuk umpan balik/diskusi tentang proposal Go 2 Generics yang sedang berjalan/terkini yang baru-baru ini diumumkan?

Singkatnya, saya sangat menyukai arah proposal pada umumnya dan mekanisme kontrak pada khususnya. Tapi saya prihatin dengan apa yang tampaknya menjadi asumsi tunggal sepanjang waktu kompilasi bahwa parameter generik harus (selalu) menjadi parameter tipe. Saya telah menulis beberapa umpan balik tentang masalah ini di sini:

Apakah Hanya Tipe Parameter Generik yang Cukup untuk Go 2 Generics?

Tentu saja komentar di sini baik-baik saja, tetapi secara umum saya tidak berpikir masalah GitHub adalah format yang baik untuk diskusi, karena mereka tidak menyediakan threading apa pun. Saya pikir milis lebih baik.

Saya rasa belum jelas seberapa sering orang ingin fungsi parameter pada nilai konstan. Kasus yang paling jelas adalah untuk dimensi array--tetapi Anda sudah dapat melakukannya dengan melewatkan tipe array yang diinginkan sebagai argumen tipe. Selain kasus itu, apa yang sebenarnya kita peroleh dengan melewatkan const sebagai argumen waktu kompilasi daripada argumen run-time?

Go sudah menawarkan banyak cara berbeda dan hebat untuk menyelesaikan masalah dan kita tidak boleh menambahkan sesuatu yang baru kecuali jika itu memperbaiki masalah dan kekurangan yang sangat besar, yang jelas-jelas tidak dilakukan, dan bahkan dalam keadaan seperti itu, kompleksitas tambahan yang mengikuti adalah sangat harga tinggi yang harus dibayar.

Go itu unik karena cara itu. Jika tidak rusak, maka jangan mencoba untuk memperbaikinya!

Orang-orang yang tidak senang dengan cara Go dirancang harus pergi dan menggunakan salah satu dari banyak bahasa lain yang sudah memiliki kerumitan tambahan dan menjengkelkan ini.

Go itu unik karena cara itu. Jika tidak rusak, maka jangan mencoba untuk memperbaikinya!

Sudah rusak, jadi harus diperbaiki.

Sudah rusak, jadi harus diperbaiki.

Ini mungkin tidak bekerja seperti yang Anda pikirkan—tetapi bahasa tidak akan pernah bisa. Ini tentu saja tidak rusak. Mempertimbangkan informasi dan debat yang tersedia, maka meluangkan waktu untuk membuat keputusan yang terinformasi dan masuk akal selalu merupakan pilihan terbaik. Banyak bahasa lain telah menderita, menurut pendapat saya, karena menambahkan lebih banyak fitur untuk memecahkan lebih banyak masalah potensial. Ingatlah bahwa "tidak" bersifat sementara, "ya" bersifat selamanya.

Setelah berpartisipasi dalam mega-isu masa lalu, bolehkah saya menyarankan agar saluran dibuka di Gopher Slack bagi mereka yang ingin membahas ini, masalah ini dikunci sementara, dan kemudian waktu diposting ketika masalah akan dicairkan untuk siapa saja yang ingin mengkonsolidasikan diskusi dari Slack? Masalah Github tidak lagi berfungsi sebagai forum setelah tautan "478 item tersembunyi Muat lebih banyak ..." yang ditakuti masuk.

bolehkah saya menyarankan agar saluran dibuka di Gopher Slack bagi mereka yang ingin membahas ini
Milis lebih baik karena menyediakan arsip yang dapat dicari. Ringkasan masih dapat diposting tentang masalah ini.

Setelah berpartisipasi dalam mega-isu masa lalu, bolehkah saya menyarankan agar saluran dibuka di Gopher Slack bagi mereka yang ingin membahas ini

Tolong jangan pindahkan diskusi sepenuhnya ke platform tertutup. Jika di mana saja, golang-nut tersedia untuk semua (ish? Saya tidak tahu apakah itu benar-benar berfungsi tanpa akun Google, tetapi setidaknya itu adalah metode komunikasi standar yang dimiliki atau dapat diperoleh semua orang) dan itu harus dipindahkan ke sana . GitHub cukup buruk, tetapi saya dengan enggan menerima bahwa kami terjebak dengannya untuk komunikasi, tidak semua orang bisa mendapatkan akun Slack atau dapat menggunakan klien mereka yang buruk.

tidak semua orang bisa mendapatkan akun Slack atau dapat menggunakan klien mereka yang mengerikan

Apa yang dimaksud dengan "bisa" di sini? Apakah ada batasan nyata pada Slack yang tidak saya ketahui atau apakah orang tidak suka menggunakannya? Yang terakhir baik-baik saja, saya kira, tetapi beberapa orang juga memboikot Github karena mereka tidak menyukai Microsoft, jadi Anda kehilangan beberapa orang tetapi mendapatkan yang lain.

tidak semua orang bisa mendapatkan akun Slack atau dapat menggunakan klien mereka yang mengerikan

Apa yang dimaksud dengan "bisa" di sini? Apakah ada batasan nyata pada Slack yang tidak saya ketahui atau apakah orang tidak suka menggunakannya? Yang terakhir baik-baik saja, saya kira, tetapi beberapa orang juga memboikot Github karena mereka tidak menyukai Microsoft, jadi Anda kehilangan beberapa orang tetapi mendapatkan yang lain.

Slack adalah perusahaan AS dan dengan demikian akan mengikuti kebijakan luar negeri yang diberlakukan oleh AS.

Github memiliki masalah yang sama dan baru saja menjadi berita karena mengusir orang Iran tanpa peringatan. Sangat disayangkan, tetapi kecuali kami menggunakan Tor atau IPFS atau semacamnya, kami harus menghormati hukum AS/Eropa untuk forum diskusi praktis apa pun.

Github memiliki masalah yang sama dan baru saja menjadi berita karena mengusir orang Iran tanpa peringatan. Sangat disayangkan, tetapi kecuali kami menggunakan Tor atau IPFS atau semacamnya, kami harus menghormati hukum AS/Eropa untuk forum diskusi praktis apa pun.

Ya, kami terjebak dengan GitHub dan Google Groups. Mari kita tidak menambahkan lebih banyak layanan bermasalah ke dalam daftar. Obrolan juga bukan arsip yang bagus; cukup sulit untuk menggali melalui diskusi ini ketika mereka dijalin dengan baik dan di golang-nut (di mana mereka langsung masuk ke kotak masuk Anda). Slack berarti jika Anda tidak berada di zona waktu yang sama dengan orang lain, Anda harus mengarungi banyak arsip obrolan, satu non-sequiters, dll. milis berarti Anda memilikinya setidaknya agak terorganisir dalam utas, dan orang cenderung mengambil lebih banyak waktu dalam balasan mereka sehingga Anda tidak mendapatkan banyak komentar acak 1 kali yang ditinggalkan begitu saja. Saya juga tidak memiliki akun Slack dan klien bodoh mereka tidak akan bekerja pada mesin yang saya gunakan. Mutt di sisi lain (atau klien email pilihan Anda, yay standar) bekerja di mana-mana.

Harap simpan masalah ini tentang obat generik. Fakta bahwa pelacak masalah GitHub tidak ideal untuk diskusi skala besar seperti obat generik layak untuk didiskusikan, tetapi tidak untuk masalah ini. Saya telah menandai beberapa komentar di atas sebagai "di luar topik".

Mengenai keunikan Go: Go memiliki beberapa fitur bagus tetapi tidak seunik yang dipikirkan beberapa orang. Sebagai dua contoh, CLU dan Modula-3 memiliki tujuan yang sama dan hasil yang serupa, dan keduanya mendukung obat generik dalam beberapa bentuk (sejak ~ 1975 dalam kasus CLU!) Mereka tidak memiliki dukungan industri saat ini tetapi FWIW, dimungkinkan untuk mendapatkan kompiler bekerja untuk keduanya.

beberapa pertanyaan tentang sintaks, apakah kata kunci type dalam parameter tipe diperlukan? dan apakah lebih masuk akal untuk mengadopsi <> untuk parameter tipe seperti bahasa lain? Ini mungkin membuat segalanya lebih mudah dibaca dan akrab ...

Meskipun saya tidak menentang hal itu dalam proposal, hanya mengajukan ini untuk dipertimbangkan

dari pada:

type Vector(type Element) []Element
var v Vector(int)
func (v *Vector(Element)) Push(x Element) { *v = append(*v, x) }
type VectorInt = Vector(int)

kita bisa memiliki

type Vector<Element> []Element
var v Vector<int>
func (v *Vector<Element>) Push(x Element) { *v = append(*v, x) }
type VectorInt = Vector<int>

Sintaks <> disebutkan dalam draft, @jnericks (Nama pengguna Anda sempurna untuk diskusi ini...). Argumen utama yang menentangnya adalah bahwa hal itu secara besar-besaran meningkatkan kompleksitas pengurai. Secara lebih umum, itu membuat Go menjadi bahasa yang jauh lebih sulit untuk diuraikan dengan sedikit manfaat. Kebanyakan orang setuju bahwa itu meningkatkan keterbacaan, tetapi ada ketidaksepakatan tentang apakah itu layak untuk ditukar atau tidak. Secara pribadi, saya rasa tidak.

Penggunaan kata kunci type diperlukan untuk memperjelas. Kalau tidak, sulit untuk membedakan antara func Example(T)(arg int) {} dan func Example(arg int) (int) {} .

Saya membaca proposal terbaru tentang obat generik. semua sesuai selera saya kecuali tata bahasa deklarasi kontrak.

seperti yang kita ketahui, dalam go kita selalu mendeklarasikan struct atau interface seperti ini :

type MyStruct struct {
        a int
        s string
}

type MyInterface inteface {
    Method1() err
    Method2() string
}

tetapi deklarasi kontrak dalam proposal terbaru adalah seperti ini:

contract Ordered(T) {
    T int, int8
}

contract G(Node, Edge) {
    Node Edges() []Edge
    Edge Nodes() (from Node, to Node)
}

Menurut saya, tata bahasa kontrak tidak konsisten bentuknya dengan pendekatan tradisional. bagaimana dengan tata bahasa seperti di bawah ini:

type Ordered(T) contract {
    T int, int8
}

if there is only one type parameter, the declaration above can be also wrote like this:

type Ordered contract {
    int , int8
}


if there are more than one type parameter, we have to use named parameter:

type G(Node, Edge) contract {
    Node Edges() []Edge
    Edge Nodes() (from Node, to Node)
}

sekarang bentuk kontraknya masih tradisional. kita dapat mendeklarasikan kontrak dalam blok tipe dengan struct, antarmuka:

type (
        Sequence contract {
                string, []byte
        }

    Stringer(T) contract {
        T String() string
    }

    Stringer contract { // equivalent with the above Stringer(T), single type parameter could be omitted
        String() string
    }

        MyStruct struct {
                a int
                b string
        }

    G(Node, Edge) contract {
        Node Edges() []Edge
        Edge Nodes() (from Node, to Node)
    }
)

Jadi "kontrak" menjadi kata kunci level yang sama dengan struct, interface. Perbedaannya adalah kontrak digunakan untuk mendeklarasikan tipe meta untuk tipe.

@bigwhite Kami masih mendiskusikan notasi ini. Argumen yang mendukung notasi yang disarankan dalam rancangan desain adalah bahwa kontrak bukan tipe (misalnya, seseorang tidak dapat mendeklarasikan variabel dari tipe kontrak), dan kontrak adalah entitas jenis baru dengan sia-sia yang sama dengan konstanta. , fungsi, variabel, atau tipe. Argumen yang mendukung saran Anda adalah bahwa kontrak hanyalah "tipe tipe" (atau tipe meta) dan karenanya harus mengikuti notasi yang konsisten. Argumen lain yang mendukung saran Anda adalah bahwa itu akan mengizinkan penggunaan literal kontrak "anonim" tanpa kebutuhan untuk menyatakannya secara eksplisit. Singkatnya, IMHO ini belum diselesaikan. Tapi itu juga mudah untuk berubah di jalan.

FWIW, CL 187317 mendukung kedua notasi saat ini (meskipun parameter kontrak harus ditulis dengan kontrak), misalnya:

type C contract(X) { ... }

dan

contract C (X) { ... }

diterima dan direpresentasikan dengan cara yang sama secara internal. Pendekatan yang lebih konsisten adalah:

type C(type X) contract { ... }

Kontrak bukanlah tipe. Itu bahkan bukan tipe meta, karena satu-satunya yang mengetiknya
menyangkut dirinya dengan adalah parameternya. Tidak ada tipe penerima terpisah
yang kontraknya dapat dianggap sebagai jenis meta.

Go juga memiliki deklarasi fungsi:

func Name(args) { body }

yang lebih langsung dicerminkan oleh sintaks kontrak yang diusulkan.

Ngomong-ngomong, diskusi sintaksis semacam ini tampaknya rendah dalam daftar prioritas di
titik ini. Lebih penting untuk melihat semantik draf dan
bagaimana pengaruhnya terhadap kode, jenis kode apa yang dapat ditulis berdasarkan itu
semantik, dan kode apa yang tidak bisa.

Sunting: Mengenai kontrak in-line, Go memiliki literal fungsi. Saya tidak melihat alasan apa pun tidak boleh ada kontrak literal. Hanya akan ada lebih sedikit tempat yang bisa mereka munculkan, karena mereka bukan tipe atau nilai.

@stevenblenkinsop Saya tidak akan mengatakan dengan jujur ​​bahwa kontrak bukanlah tipe (atau tipe meta). Saya pikir ada argumen yang sangat masuk akal untuk kedua sudut pandang. Misalnya, kontrak parameter tunggal yang hanya menetapkan metode pada dasarnya berfungsi sebagai "batas atas" untuk parameter tipe: Setiap argumen tipe yang valid harus mengimplementasikan metode tersebut. Untuk itulah kita biasanya menggunakan antarmuka. Mungkin masuk akal untuk mengizinkan antarmuka dalam kasus tersebut alih-alih kontrak, a) karena kasus ini mungkin umum; dan b) karena memenuhi kontrak dalam hal ini berarti memenuhi antarmuka yang dijabarkan sebagai kontrak. Artinya, kontrak semacam itu sangat mirip dengan tipe yang dengannya tipe lain "dibandingkan".

@griesemer mempertimbangkan kontrak sebagai tipe dapat menyebabkan masalah dengan paradoks Russel (seperti pada tipe semua tipe yang bukan 'anggota' sendiri). Saya pikir mereka lebih baik dianggap 'kendala pada tipe'. Jika kita menganggap sistem tipe sebagai bentuk 'logika', kita dapat membuat prototipe ini di Prolog. Variabel tipe menjadi variabel logika, tipe menjadi atom, dan kontrak/batasan dapat diselesaikan dengan Pemrograman Logika Kendala. Semuanya sangat rapi dan tidak paradoks. Dalam hal sintaks, kita dapat menganggap kontrak sebagai fungsi pada tipe yang mengembalikan boolean.

@keean Antarmuka apa pun sudah berfungsi sebagai "kendala pada tipe", namun itu adalah tipe. Teori tipe orang sangat melihat batasan tipe sebagai tipe, dengan cara yang sangat formal. Seperti yang telah saya sebutkan di atas , ada argumen yang masuk akal yang dapat dibuat untuk kedua sudut pandang. Tidak ada "paradoks logika" di sini - sebenarnya prototipe pekerjaan yang sedang berjalan saat ini memodelkan kontrak sebagai tipe internal karena menyederhanakan masalah saat ini.

Antarmuka @griesemer di Go adalah 'subtipe' bukan batasan pada tipe. Namun saya menemukan kebutuhan untuk kontrak dan antarmuka merugikan desain Go, namun mungkin sudah terlambat untuk mengubah antarmuka menjadi batasan tipe daripada subtipe. Saya telah berpendapat di atas bahwa antarmuka Go tidak harus berupa subtipe, tetapi saya tidak melihat banyak dukungan untuk gagasan itu. Ini akan memungkinkan antarmuka dan kontrak menjadi hal yang sama - jika antarmuka dapat dideklarasikan untuk operator juga.

Ada paradoks di sini, jadi berhati-hatilah, Paradoks Girard adalah 'pengkodean' yang paling umum dari Paradoks Russel ke dalam teori tipe. Teori tipe memperkenalkan konsep alam semesta untuk mencegah paradoks ini, dan Anda hanya diperbolehkan untuk merujuk jenis di alam semesta 'U' dari alam semesta 'U+1'. Secara internal teori tipe ini diimplementasikan sebagai logika tingkat tinggi (misalnya Elf menggunakan lambda-prolog). Ini pada gilirannya mengurangi pemecahan kendala untuk subset yang dapat diputuskan dari logika orde tinggi.

Jadi, meskipun Anda dapat menganggapnya sebagai tipe, Anda perlu menambahkan serangkaian batasan penggunaan (sintaksis atau lainnya) yang secara efektif membuat Anda kembali ke batasan tipe. Saya pribadi merasa lebih mudah untuk bekerja secara langsung dengan batasan, dan menghindari dua lapisan abstraksi lebih lanjut, logika tingkat tinggi, dan tipe dependen. Abstraksi ini tidak menambah kekuatan ekspresif dari sistem tipe, dan memerlukan aturan atau batasan lebih lanjut untuk mencegah paradoks.

Mengenai prototipe saat ini yang memperlakukan batasan sebagai tipe, bahaya muncul jika Anda dapat menggunakan "tipe batasan" ini sebagai tipe normal, dan kemudian membuat 'tipe batasan' lain pada tipe itu. Anda akan memerlukan pemeriksaan untuk mencegah referensi diri (ini biasanya sepele) dan loop referensi bersama. Prototipe semacam ini harus benar-benar ditulis dalam Prolog, karena memungkinkan Anda untuk fokus pada aturan implementasi. Saya percaya para pengembang Rust akhirnya menyadari hal ini beberapa waktu lalu (lihat Kapur).

@griesemer Menarik, memodelkan kembali kontrak sebagai tipe. Dari model mental saya sendiri, saya akan menganggap batasan sebagai metatipe, dan kontrak sebagai semacam struktur tingkat tipe.

type A int
func (a A) Foo() int {
    return int(a)
}

type C contract(T, U) {
    T int
    U int, uint
    U Foo() int
}

var B (int, uint; Foo() int).type = A
var C1 C = C(A, B)

Ini menunjukkan kepada saya bahwa sintaks gaya deklarasi tipe saat ini untuk kontrak adalah yang lebih benar dari keduanya. Saya pikir sintaks yang ditetapkan dalam draf masih lebih baik, karena tidak perlu menjawab pertanyaan "jika itu adalah tipe seperti apa nilainya".

@stevenblenkinsop Anda kehilangan saya, mengapa Anda meneruskan T ke C contract ketika tidak digunakan, dan apa yang coba dilakukan oleh baris var ?

@griesemer terima kasih atas balasan Anda. Salah satu prinsip desain Go adalah "hanya menyediakan satu cara untuk melakukan sesuatu". Lebih baik menyimpan hanya satu formulir pernyataan kontrak. kontrak tipe C(tipe X) { ... } lebih baik.

@Goodwine Saya telah mengganti nama tipe untuk membedakannya dari parameter kontrak. Mungkin itu membantu? (int, uint; Foo() int).type dimaksudkan untuk menjadi metatipe dari tipe apa pun yang memiliki tipe dasar int atau uint dan yang mengimplementasikan Foo() int . var B dimaksudkan untuk menunjukkan menggunakan tipe sebagai nilai, dan menetapkannya ke variabel yang tipenya adalah metatipe (karena metatipe seperti tipe yang nilainya adalah tipe). var C1 dimaksudkan untuk menunjukkan variabel yang tipenya adalah kontrak, dan menunjukkan contoh sesuatu yang mungkin ditugaskan ke variabel semacam itu. Pada dasarnya, mencoba menjawab pertanyaan "jika kontrak adalah tipe, seperti apa nilainya?". Intinya adalah untuk menunjukkan bahwa nilai itu tampaknya bukan tipe.

Saya mendapat masalah dengan kontrak dengan beberapa jenis.

Anda dapat menambahkan atau meninggalkannya untuk kontrak jenis paremeter, keduanya
type Graph (type Node, Edge) struct { ... }
dan
type Graph (type Node, Edge G) struct { ... } baik-baik saja.

Tetapi bagaimana jika saya hanya ingin menambahkan kontrak pada salah satu dari dua jenis parameter?

contract G(Node, Edge) {
    Node Edges() []Edge
    Edge Nodes() (from Node, to Node)
}

VS

contract G(Edge) {
    Edge Nodes() (from Node, to Node)
}

@themez Itu ada di draft. Anda dapat menggunakan sintaks (type T, U comparable(T)) untuk membatasi hanya satu jenis parameter, misalnya.

@stevenblenkinsop begitu, terima kasih.

@themez Ini telah muncul beberapa kali sekarang. Saya pikir ada beberapa kebingungan dari fakta bahwa penggunaannya terlihat seperti tipe untuk definisi variabel. Ini benar-benar tidak; kontrak lebih merupakan detail dari seluruh fungsi daripada definisi argumen. Saya pikir asumsinya adalah bahwa Anda pada dasarnya akan menulis kontrak baru, berpotensi terdiri dari kontrak lain untuk membantu pengulangan, untuk pada dasarnya setiap fungsi/tipe generik yang Anda buat. Hal-hal seperti yang disebutkan @stevenblenkinsop benar-benar ada untuk menangkap kasus-kasus Edge di mana asumsi itu tidak masuk akal.

Setidaknya, itulah kesan yang saya dapatkan, terutama dari fakta bahwa mereka disebut 'kontrak'.

@keean Saya pikir kami menafsirkan kata "kendala" secara berbeda; Saya menggunakannya agak informal. Menurut definisi antarmuka, diberikan antarmuka I , dan variabel x bertipe I , hanya nilai dengan tipe yang mengimplementasikan I yang dapat ditetapkan ke x . Jadi I dapat dilihat sebagai "kendala" pada tipe tersebut (tentu saja masih ada banyak tipe yang memenuhi "batasan" itu). Demikian pula, seseorang dapat menggunakan I sebagai batasan untuk parameter tipe P dari fungsi generik; hanya argumen tipe aktual dengan kumpulan metode yang mengimplementasikan I yang diizinkan. Jadi I juga membatasi kumpulan kemungkinan tipe argumen aktual.

Dalam kedua kasus, alasannya adalah untuk menjelaskan operasi (metode) yang tersedia di dalam fungsi. Jika I digunakan sebagai tipe parameter (nilai), kita tahu bahwa parameter menyediakan metode tersebut. Jika I us digunakan sebagai "kendala" (sebagai pengganti kontrak), kita tahu bahwa semua nilai parameter tipe yang dibatasi menyediakan metode tersebut. Ini jelas cukup lurus ke depan.

Saya ingin contoh nyata mengapa ide spesifik menggunakan antarmuka untuk kontrak parameter tunggal yang hanya mendeklarasikan metode "rusak" tanpa beberapa batasan seperti yang Anda singgung dalam komentar Anda.

Bagaimana proposal kontrak akan diperkenalkan? Menggunakan parameter go modules go1.14 ? Variabel lingkungan GO114CONTRACTS ? Keduanya? Sesuatu yang lain..?

Maaf jika ini telah ditangani sebelumnya, jangan ragu untuk mengarahkan saya ke sana.

Satu hal yang saya sangat suka tentang desain draf generik saat ini adalah ia menempatkan air jernih antara contracts dan interfaces . Saya merasa ini penting karena kedua konsep itu mudah dikacaukan meskipun ada tiga perbedaan mendasar di antara mereka:

  1. Contracts menjelaskan persyaratan _set_ tipe, sedangkan interfaces menjelaskan metode yang harus dipenuhi oleh tipe _single_.

  2. Contracts dapat menangani operasi bawaan, konversi, dll. dengan mencantumkan jenis yang mendukungnya; interfaces hanya dapat menangani metode yang tidak dimiliki oleh tipe bawaan itu sendiri.

  3. Apa pun itu dalam istilah teoretis tipe, contracts bukanlah tipe seperti yang biasanya kita pikirkan di Go yaitu Anda tidak dapat mendeklarasikan variabel dari tipe contract dan memberi mereka beberapa nilai. Di sisi lain interfaces adalah tipe, Anda dapat mendeklarasikan variabel dari tipe tersebut dan menetapkan nilai yang sesuai untuknya.

Meskipun saya dapat melihat arti dari contract , yang memerlukan parameter tipe tunggal untuk memiliki metode tertentu, untuk diwakili sebagai gantinya oleh interface (itu adalah sesuatu yang bahkan saya anjurkan di masa lalu saya sendiri proposal), saya merasa sekarang ini akan menjadi langkah yang tidak menguntungkan karena akan kembali memperkeruh perairan antara contracts dan interfaces .

Tidak terpikir oleh saya sebelumnya bahwa contracts secara masuk akal dapat dideklarasikan dengan cara yang disarankan @bigwhite menggunakan pola 'tipe' yang ada. Namun, sekali lagi saya tidak tertarik dengan ide tersebut karena saya merasa itu akan berkompromi (3) di atas. Juga, jika perlu (untuk alasan penguraian) ulangi kata kunci type saat mendeklarasikan struct generik seperti:

type List(type Element) struct {
    next *List(Element)
    val  Element
}

mungkin juga perlu untuk mengulanginya jika contracts dideklarasikan dengan cara yang sama yang sedikit 'gagap' dibandingkan dengan pendekatan desain draf.

Ide lain yang saya tidak tertarik adalah `contract literals' yang memungkinkan contracts untuk ditulis 'di tempat' daripada sebagai konstruksi terpisah. Ini akan membuat definisi fungsi dan tipe generik lebih sulit dibaca dan, seperti yang dipikirkan beberapa orang, itu tidak akan membantu meyakinkan orang-orang itu bahwa generik adalah hal yang baik.

Maaf tampak begitu menolak perubahan yang diusulkan pada draf obat generik (yang memang memiliki beberapa masalah), tetapi, sebagai pendukung antusias obat generik sederhana untuk Go, saya merasa poin ini layak untuk dibuat.

Saya ingin menyarankan untuk tidak memanggil predikat di atas jenis "kontrak". Ada dua alasan:

  • Istilah "kontrak" sudah digunakan dalam ilmu komputer dengan cara yang berbeda. Misalnya, lihat: (https://scholar.google.com/scholar?hl=id&as_sdt=0%2C33&q=contracts+languages&btnG=)
  • Sudah ada beberapa nama untuk ide ini dalam literatur ilmu komputer. Saya tahu setidaknya ~tiga~ empat: "typesets", "type class", "concepts", dan "constraints". Menambahkan yang lain hanya akan membingungkan masalah lebih lanjut.

@griesemer "kendala pada tipe" adalah hal waktu kompilasi murni, karena tipe terhapus sebelum runtime. Kendala tersebut menyebabkan kode generik dijabarkan menjadi kode non-generik yang dapat dieksekusi. Subtipe ada saat runtime, dan bukan kendala dalam arti bahwa batasan pada tipe akan menjadi tipe-kesetaraan atau ketidaksetaraan tipe minimal, dengan batasan seperti 'adalah subtipe' tersedia secara opsional tergantung pada sistem tipe.

Bagi saya sifat runtime dari subtipe adalah perbedaan kritis, jika X <: Y kita dapat melewati X di mana Y diharapkan tetapi kita hanya mengetahui tipenya sebagai Y tanpa operasi runtime yang tidak aman. Dalam hal ini tidak membatasi tipe Y, Y selalu Y. Subtipe juga 'terarah' karenanya dapat menjadi kovarian atau kontravarian tergantung pada apakah itu diterapkan pada argumen input atau output.

Dengan batasan tipe 'pred(X)', kita mulai dengan X polimorfik penuh, dan kemudian kita membatasi nilai yang diizinkan. Jadi katakan hanya X yang mengimplementasikan 'print'. Ini non-directional dan karenanya tidak memiliki co-variance atau kontravarian. Infact invarian karena kita mengetahui tipe dasar X pada waktu kompilasi.

Jadi saya pikir berbahaya untuk menganggap antarmuka sebagai batasan pada tipe karena mengabaikan perbedaan penting seperti kovarians dan kontravarian.

Apakah itu menjawab pertanyaan Anda, atau apakah saya melewatkan intinya?

Sunting: Saya harus menunjukkan bahwa saya mengacu pada antarmuka 'Pergi' secara khusus di atas. Poin tentang subtipe berlaku untuk semua bahasa yang memiliki subtipe, tetapi Go tidak biasa dalam membuat antarmuka menjadi tipe dan karenanya memiliki hubungan subtipe. Dalam bahasa lain seperti Java, antarmuka secara eksplisit bukan tipe (kelas adalah tipe) sehingga antarmuka _merupakan kendala pada tipe. Jadi, meskipun secara umum menganggap antarmuka sebagai batasan pada tipe adalah benar, itu salah secara khusus untuk 'Go'.

@Inuart Terlalu dini untuk mengatakan bagaimana ini akan ditambahkan ke implementasi. Belum ada proposal, baru draf desain. Itu pasti tidak akan di 1,14.

@andrewcmyers Saya suka kata "kontrak" karena menggambarkan hubungan antara penulis fungsi generik dan pemanggilnya.

Kata-kata seperti "typesets" dan "type class" menunjukkan bahwa kita berbicara tentang meta-type, yang tentu saja kita, tetapi kontrak juga menggambarkan hubungan antara beberapa jenis. Saya tahu bahwa kelas tipe di, misalnya, Haskell, dapat memiliki beberapa parameter tipe, tetapi bagi saya tampaknya nama tersebut kurang cocok untuk ide yang sedang dijelaskan.

Saya tidak pernah mengerti mengapa C++ menyebut ini "konsep." Apa artinya itu?

"Kendala" atau "kendala" akan baik-baik saja dengan saya. Saat ini saya menganggap kontrak mengandung banyak batasan. Tapi kita bisa mengubah pemikiran itu.

Saya tidak terlalu peduli dengan fakta bahwa ada konstruksi bahasa pemrograman yang disebut "kontrak". Saya menganggap ide itu relatif mirip dengan ide yang ingin kita ungkapkan, karena ini adalah hubungan antara fungsi dan pemanggilnya. Saya mengerti bahwa cara hubungan itu diekspresikan sangat berbeda, tetapi saya merasa ada kesamaan yang mendasarinya.

Saya tidak pernah mengerti mengapa C++ menyebut ini "konsep." Apa artinya itu?

Konsep adalah abstraksi dari instantiasi yang berbagi beberapa kesamaan, misalnya tanda tangan.

Istilah konsep sejauh ini lebih cocok untuk antarmuka karena yang terakhir juga digunakan untuk menunjukkan batas bersama antara dua komponen.

@sighoya Saya juga akan menyebutkan bahwa 'konsep' adalah konseptual karena mereka menyertakan 'aksioma' yang penting untuk mencegah penyalahgunaan operator. Misalnya penambahan '+' harus asosiatif dan komutatif. Aksioma ini tidak dapat direpresentasikan dalam C++, maka mereka ada sebagai ide abstrak, maka 'konsep'. Jadi sebuah konsep adalah 'kontrak' sintaksis ditambah aksioma semantik.

@ianlancetaylor "Constraint" adalah apa yang kami sebut di Genus (http://www.cs.cornell.edu/~yizhou/papers/genus-pldi2015.pdf), jadi saya tidak setuju dengan terminologi itu. Istilah "kontrak" akan menjadi pilihan yang benar-benar masuk akal, kecuali bahwa itu digunakan sangat aktif di komunitas PL untuk merujuk pada hubungan antara antarmuka dan implementasi, yang juga memiliki rasa kontrak.

@keean Tanpa menjadi ahli, saya tidak berpikir dikotomi yang Anda lukis mencerminkan kenyataan dengan sangat baik. Misalnya, apakah kompiler menghasilkan versi turunan dari fungsi generik sepenuhnya merupakan masalah implementasi, jadi sangat masuk akal untuk memiliki representasi runtime dari kendala, katakanlah dalam bentuk tabel pointer fungsi untuk setiap operasi yang diperlukan. Persis seperti tabel metode antarmuka, sebenarnya. Demikian juga, antarmuka di Go tidak sesuai dengan definisi subtipe Anda, karena Anda dapat memproyeksikannya kembali dengan aman (melalui pernyataan tipe) dan karena Anda tidak memiliki ko- atau kontravarian untuk konstruktor tipe apa pun di Go.

Terakhir: Apakah dikotomi yang Anda lukis realistis dan akurat, tidak mengubah bahwa antarmuka, pada akhirnya, hanya daftar metode - dan bahkan dalam dikotomi Anda, tidak ada alasan mengapa daftar itu bisa 't dapat digunakan kembali sebagai tabel yang diwakili waktu proses atau batasan waktu kompilasi saja, tergantung pada konteks penggunaannya.

Bagaimana dengan sesuatu seperti:

typeConstraint C(T) {
}

atau

tipeKontrak C(T) {
}

Ini berbeda dari deklarasi tipe lain untuk menekankan bahwa ini bukan konstruksi runtime.

Tentang desain kontrak baru, saya punya beberapa pertanyaan.

1.

Ketika tipe generik A menyematkan tipe generik B lainnya,
atau fungsi generik A memanggil fungsi generik lain B,
apakah kita perlu juga menentukan kontrak B pada A?

Jika jawabannya benar, maka jika tipe generik menyematkan banyak tipe geneirc lainnya,
atau fungsi generik memanggil banyak fungsi generik lainnya,
maka kita perlu menggabungkan banyak kontrak menjadi satu sebagai kontrak tipe embedding atau fungsi pemanggil.
Hal ini dapat menyebabkan masalah const-keracunan yang sama.

  1. Selain batasan jenis dan metode set saat ini, apakah kita memerlukan batasan lain?
    Seperti konversi dari satu jenis ke jenis lainnya, dapat dialihkan dari satu jenis ke jenis lainnya,
    sebanding antara dua jenis, adalah saluran yang dapat dikirim, adalah saluran yang dapat diterima,
    memiliki set bidang tertentu, ...

3.

Jika fungsi generik menggunakan garis seperti berikut

v.Foo()

Bagaimana kita bisa menulis kontrak yang memungkinkan Foo menjadi metode atau bidang dari tipe fungsi?

@merovius type-constraints harus diselesaikan pada waktu kompilasi, atau sistem tipe bisa tidak sehat. Ini karena Anda dapat memiliki tipe yang bergantung pada tipe lain yang tidak diketahui hingga runtime. Anda kemudian memiliki dua pilihan, Anda harus menerapkan sistem tipe dependen penuh (yang memungkinkan pemeriksaan tipe terjadi saat runtime saat tipe diketahui) atau Anda harus menambahkan tipe eksistensial ke sistem tipe. Eksistensial mengkodekan perbedaan fase dari tipe yang diketahui secara statis, dan tipe yang hanya diketahui saat runtime (tipe yang bergantung pada pembacaan dari IO misalnya).

Subtipe seperti yang disebutkan di atas, biasanya tidak diketahui hingga runtime, meskipun banyak bahasa memiliki pengoptimalan jika tipenya diketahui secara statis.

Jika kita menganggap salah satu perubahan di atas adalah memperkenalkan bahasa (tipe dependen atau tipe eksistensial) maka kita masih perlu memisahkan konsep subtipe dan batasan tipe. Untuk Go secara khusus tipe-konstriktor adalah invarian, kami dapat mengabaikan perbedaan ini, dan kami dapat mempertimbangkan bahwa antarmuka-Go _are_ kendala pada tipe (secara statis).

Oleh karena itu, kita dapat menganggap antarmuka-Go sebagai kontrak parameter tunggal di mana parameternya adalah penerima semua fungsi/metode. Jadi mengapa go memiliki antarmuka dan kontrak? Tampaknya bagi saya karena Go tidak ingin mengizinkan antarmuka untuk operator (seperti '+'), dan karena Go tidak memiliki tipe dependen atau tipe eksistensial.

Jadi ada dua faktor yang membuat perbedaan nyata antara batasan tipe dan subtipe. Salah satunya adalah co/contra-variance, yang mungkin dapat kita abaikan di Go karena invarians konstruktor tipe, dan yang lainnya adalah kebutuhan akan tipe dependen atau tipe eksistensial untuk membuat sistem tipe yang memiliki batasan tipe terdengar jika ada polimorfisme runtime dari parameter tipe ke batasan tipe.

@keean Keren, jadi AIUI kami setidaknya setuju bahwa antarmuka di Go dapat dianggap sebagai kendala :)

Mengenai sisanya: Di atas Anda mengklaim:

"batasan pada tipe" adalah hal waktu kompilasi murni, karena tipe terhapus sebelum runtime. Kendala tersebut menyebabkan kode generik dijabarkan menjadi kode non-generik yang dapat dieksekusi.

Klaim itu lebih spesifik daripada klaim terbaru Anda, bahwa kendala perlu diselesaikan pada waktu kompilasi. Yang ingin saya katakan, adalah bahwa kompiler dapat melakukan resolusi itu (dan semua pemeriksaan tipe yang sama), tetapi kemudian masih menghasilkan kode generik. Itu akan tetap terdengar, karena semantik sistem tipenya sama. Tetapi kendalanya masih memiliki representasi run-time. Itu agak rewel - tapi itu sebabnya saya merasa mendefinisikan ini berdasarkan run-time vs waktu kompilasi bukanlah cara terbaik untuk melakukannya. Ini mencampurkan masalah implementasi ke dalam diskusi tentang semantik abstrak dari sistem tipe.

FWIW, saya telah berpendapat sebelumnya bahwa saya lebih suka menggunakan antarmuka untuk mengekspresikan kendala - dan juga sampai pada kesimpulan, bahwa mengizinkan penggunaan operator dalam kode generik adalah blok jalan utama untuk melakukan itu dan dengan demikian alasan utama untuk memperkenalkan yang terpisah konsep dalam bentuk kontrak.

@keean Terima kasih, tapi tidak, balasan Anda tidak menjawab pertanyaan saya. Perhatikan bahwa dalam komentar saya, saya menjelaskan contoh yang sangat sederhana menggunakan antarmuka sebagai pengganti kontrak/"kendala" yang sesuai. Saya meminta contoh _simple_ _concrete_ mengapa skenario ini tidak berfungsi "tanpa batasan" seperti yang Anda singgung dalam komentar Anda sebelumnya. Anda tidak memberikan contoh seperti itu.

Perhatikan bahwa saya tidak menyebutkan subtipe, co- atau kontra-varians (yang tidak kami izinkan di Go, tanda tangan harus selalu cocok), dll. Sebaliknya, saya telah menggunakan terminologi Go dasar dan mapan (antarmuka, implementasi, ketik parameter, dll.) untuk menjelaskan apa yang saya maksud dengan "kendala" karena itu adalah bahasa umum yang dipahami semua orang di sini dan semua orang dapat mengikuti. (Juga, bertentangan dengan klaim Anda di sini , di Jawa, antarmuka terlihat seperti tipe menurut saya menurut spesifikasi Java : "Deklarasi antarmuka menentukan tipe referensi bernama baru". Jika ini tidak mengatakan antarmuka adalah tipe, maka orang-orang Java Spec memiliki beberapa pekerjaan yang harus dilakukan.)

Tetapi sepertinya Anda menjawab pertanyaan saya secara tidak langsung dengan komentar terbaru Anda, seperti yang telah diamati oleh @Merovius , ketika Anda mengatakan: "Karena itu, kami dapat menganggap antarmuka-Go sebagai kontrak parameter tunggal di mana parameternya adalah penerima semua fungsi/metode .". Ini persis poin yang saya buat di awal, jadi terima kasih telah mengkonfirmasi apa yang saya katakan selama ini.

@dotaheor

Ketika tipe generik A menyematkan tipe generik B lainnya, atau fungsi generik A memanggil fungsi generik lain B, apakah kita juga perlu menentukan kontrak B pada A?

Jika tipe generik A menyematkan tipe generik B lainnya, maka parameter tipe yang diteruskan ke B harus memenuhi kontrak apa pun yang digunakan oleh B. Untuk melakukannya, kontrak yang digunakan oleh A harus menyiratkan kontrak yang digunakan oleh B. Artinya, semua batasan pada parameter tipe yang diteruskan ke B harus dinyatakan dalam kontrak yang digunakan oleh A. Hal ini juga berlaku ketika fungsi generik memanggil fungsi generik lainnya.

Jika jawabannya benar, maka jika tipe generik menyematkan banyak tipe geneirc lain, atau fungsi generik memanggil banyak fungsi generik lainnya, maka kita perlu menggabungkan banyak kontrak menjadi satu sebagai kontrak tipe embedding atau fungsi pemanggil. Hal ini dapat menyebabkan masalah const-keracunan yang sama.

Saya pikir apa yang Anda katakan itu benar, tetapi itu bukan masalah keracunan racun. Masalah const-poisoning adalah Anda harus menyebarkan const ke mana pun argumen dilewatkan, dan kemudian jika Anda menemukan tempat di mana argumen harus diubah, Anda harus menghapus const di mana-mana. Kasus dengan obat generik lebih seperti "jika Anda memanggil beberapa fungsi, Anda harus meneruskan nilai dari tipe yang benar ke masing-masing fungsi tersebut."

Bagaimanapun, bagi saya tampaknya sangat tidak mungkin bahwa orang akan menulis fungsi generik yang memanggil banyak fungsi generik lainnya yang semuanya menggunakan kontrak yang berbeda. Bagaimana itu bisa terjadi secara alami?

Selain batasan jenis dan metode set saat ini, apakah kita memerlukan batasan lain? Seperti konversi dari satu jenis ke jenis lainnya, dapat dialihkan dari satu jenis ke jenis lainnya, dapat dibandingkan antara dua jenis, adalah saluran yang dapat dikirim, adalah saluran yang dapat diterima, memiliki kumpulan bidang tertentu, ...

Batasan seperti konvertibilitas dan penugasan dan keterbandingan diekspresikan dalam bentuk tipe, seperti yang dijelaskan dalam draf desain. Batasan seperti saluran yang dapat dikirim atau diterima hanya dapat dinyatakan dalam bentuk chan T di mana T adalah beberapa jenis parameter, seperti yang dijelaskan dalam rancangan desain. Tidak ada cara untuk menyatakan batasan bahwa suatu tipe memiliki kumpulan bidang yang ditentukan, tetapi saya ragu itu akan sering muncul. Kita harus melihat bagaimana ini bekerja dengan menulis kode nyata untuk melihat apa yang terjadi.

Jika fungsi generik menggunakan garis seperti berikut

v.Foo()
Bagaimana kita bisa menulis kontrak yang memungkinkan Foo menjadi metode atau bidang dari tipe fungsi?

Dalam draf desain saat ini, Anda tidak bisa. Apakah itu tampak seperti kasus penggunaan yang penting? (Saya tahu bahwa rancangan desain sebelumnya memang mendukung ini.)

@griesemer Anda melewatkan poin di mana saya mengatakan itu hanya valid jika Anda memasukkan tipe dependen atau tipe eksistensial ke dalam sistem tipe.

Jika tidak, jika Anda menggunakan kontrak sebagai antarmuka, Anda bisa gagal saat runtime, karena Anda perlu menunda pengecekan tipe sampai Anda mengetahui tipenya, dan pengecekan tipe bisa gagal, yang karenanya tidak aman untuk tipe.

Saya juga telah melihat antarmuka yang dijelaskan sebagai subtipe, jadi Anda harus berhati-hati agar seseorang tidak mencoba dan memperkenalkan co/contra-variance ke dalam konstruktor tipe di masa mendatang. Lebih baik tidak memiliki antarmuka sebagai tipe, maka tidak ada kemungkinan ini, dan niat para desainer, bahwa ini bukan subtipe, sudah jelas.

Bagi saya itu akan menjadi desain yang lebih baik untuk menggabungkan antarmuka dan kontrak, dan membuatnya secara eksplisit mengetik batasan (predikat pada tipe).

@ianlancetaylor

Bagaimanapun, bagi saya tampaknya sangat tidak mungkin bahwa orang akan menulis fungsi generik yang memanggil banyak fungsi generik lainnya yang semuanya menggunakan kontrak yang berbeda. Bagaimana itu bisa terjadi secara alami?

Mengapa itu tidak biasa? Jika saya mendefinisikan fungsi pada tipe 'T' maka saya ingin memanggil fungsi pada 'T'. Misalnya jika saya mendefinisikan fungsi 'jumlah' di atas 'tipe yang dapat ditambahkan' berdasarkan kontrak. Sekarang saya ingin membangun fungsi perkalian umum yang memanggil jumlah? Banyak hal dalam pemrograman memiliki struktur jumlah/produk (apa pun yang merupakan 'grup').

Saya tidak mengerti apa yang akan menjadi tujuan antarmuka setelah kontrak berada dalam bahasa tersebut, sepertinya kontrak akan berfungsi untuk tujuan yang sama, untuk memastikan suatu tipe memiliki serangkaian metode yang ditentukan di dalamnya.

@keean Kasus yang tidak biasa adalah fungsi yang memanggil banyak fungsi generik lainnya yang semuanya menggunakan kontrak yang berbeda . Contoh tandingan Anda hanya memanggil satu fungsi. Ingatlah bahwa saya menentang kesamaan dengan keracunan racun.

@mrkaspa Cara paling sederhana untuk dipikirkan adalah bahwa kontrak seperti fungsi template C++ dan antarmuka seperti metode virtual C++. Ada kegunaan dan tujuan keduanya.

@ianlancetaylor dari pengalaman ada dua masalah yang terjadi yang mirip dengan const keracunan. Keduanya terjadi karena sifat seperti pohon dari panggilan fungsi bersarang. Yang pertama adalah ketika Anda ingin menambahkan debugging ke fungsi yang sangat bersarang, Anda harus menambahkan printable dari daun sampai ke root, yang bisa melibatkan menyentuh beberapa perpustakaan pihak ketiga. Yang kedua adalah Anda dapat mengumpulkan sejumlah besar kontrak di root, membuat tanda tangan fungsi sulit dibaca. Seringkali lebih baik jika kompiler menyimpulkan batasan seperti yang dilakukan Haskell dengan kelas tipe untuk menghindari dua masalah ini.

@ianlancetaylor Saya tidak tahu terlalu banyak tentang c++, apa yang akan menjadi kasus penggunaan untuk antarmuka dan kontrak di golang? kapan saya harus menggunakan antarmuka atau kontrak?

@keean Subthread ini adalah tentang rancangan desain khusus untuk bahasa Go. Di Go semua nilai dapat dicetak. Itu bukan sesuatu yang perlu diungkapkan dalam kontrak. Dan sementara saya bersedia melihat bukti bahwa banyak kontrak dapat terakumulasi untuk satu fungsi atau jenis generik, saya tidak bersedia menerima pernyataan bahwa itu akan terjadi. Inti dari rancangan desain adalah mencoba menulis kode nyata yang menggunakannya.

Draf desain menjelaskan sejelas mungkin mengapa saya berpikir bahwa menyimpulkan batasan adalah pilihan yang buruk untuk bahasa seperti Go yang dirancang untuk pemrograman dalam skala besar.

@mrkaspa Misalnya, jika Anda memiliki []io.Reader maka Anda menginginkan nilai antarmuka, bukan kontrak. Sebuah kontrak akan mengharuskan semua elemen dalam irisan menjadi tipe yang sama. Antarmuka akan mengizinkan mereka menjadi tipe yang berbeda, selama semua tipe mengimplementasikan io.Reader .

@ianlancetaylor sejauh yang saya dapatkan antarmuka membuat tipe baru sementara kontrak membatasi tipe tetapi tidak membuat yang baru, apakah saya benar?

@ianlancetaylor :

Bisakah Anda tidak melakukan sesuatu seperti berikut ini?

contract Reader(T) {
  T Read([]byte) (int, error)
}

func ReadAll(type T Reader)(readers []T) ([]byte, error) {
  // Use the readers...
}

Sekarang ReadAll() seharusnya menerima []io.Reader sama seperti menerima []*os.File , bukan? io.Reader tampaknya memenuhi kontrak, dan saya tidak ingat apa pun dalam draf tentang nilai antarmuka yang tidak dapat digunakan sebagai argumen tipe.

Sunting: Tidak apa-apa. Saya salah paham. Ini masih tempat di mana Anda akan menggunakan antarmuka, jadi ini adalah jawaban untuk pertanyaan @mrkaspa . Anda hanya tidak menggunakan antarmuka dalam tanda tangan fungsi; Anda hanya menggunakannya di tempat yang dipanggil.

@mrkaspa Ya, itu benar.

@ianlancetaylor jika saya memiliki daftar []io.Reader dan kontrak ini:

contract Reader(T) {
  T Read([]byte) (int, error)
}

func ReadAll(type T Reader)(readers []T) ([]byte, error) {
  // Use the readers...
}

Saya dapat memanggil ReadAll melalui setiap antarmuka karena mereka memenuhi kontrak?

@ianlancetaylor yakin semuanya dapat dicetak, tetapi mudah untuk menemukan contoh lain, misalnya masuk ke file atau ke jaringan, kami ingin logging menjadi generik sehingga kami dapat mengubah target log antara nol, file lokal, layanan jaringan, dll. Menambahkan masuk ke fungsi daun memerlukan penambahan kendala sepanjang jalan kembali total root, termasuk harus memodifikasi perpustakaan pihak ketiga yang digunakan.

Kode tidak statis, Anda juga harus mengizinkan pemeliharaan. Sebenarnya kode dalam 'pemeliharaan' lebih lama daripada yang diperlukan untuk menulis, jadi ada argumen bagus bahwa kita harus mendesain bahasa untuk membuat pemeliharaan, pemfaktoran ulang, penambahan fitur, dll. lebih mudah.

Sungguh masalah ini hanya akan terwujud dalam basis kode besar, yang dipertahankan dari waktu ke waktu. Ini bukan sesuatu yang dapat Anda tulis dengan contoh kecil yang cepat untuk ditunjukkan.

Masalah-masalah ini juga ada dalam bahasa umum lainnya, misalnya Ada. Anda dapat mem-porting beberapa aplikasi Ada besar yang menggunakan Go secara ekstensif, tetapi jika ada masalah di Ada, saya tidak melihat apa pun di Go yang akan mengurangi masalah itu.

@mrkaspa Ya.

Pada titik ini saya menyarankan agar utas percakapan ini pindah ke golang-nuts. Pelacak masalah GitHub adalah tempat yang buruk untuk diskusi semacam ini.

@keean Mungkin Anda benar. Waktu akan menjawab. Kami secara eksplisit meminta orang untuk mencoba menulis kode ke draf desain. Ada sedikit nilai dalam diskusi hipotetis murni.

@keean Saya tidak mengerti contoh logging Anda. Masalah yang Anda gambarkan adalah sesuatu yang dapat Anda selesaikan dengan antarmuka saat runtime, bukan dengan obat generik pada waktu kompilasi.

Antarmuka @bserdar hanya memiliki satu parameter tipe, jadi Anda tidak dapat melakukan sesuatu di mana satu parameter adalah hal yang akan dicatat, dan parameter tipe kedua adalah tipe log.

@keean IMO dalam contoh itu Anda akan melakukan hal yang sama seperti yang Anda lakukan hari ini, tanpa parameter tipe sama sekali: Gunakan refleksi untuk memeriksa hal yang akan dicatat dan gunakan context.Context untuk meneruskan nilai log. Saya tahu bahwa ide-ide ini menjijikkan bagi penggemar mengetik, tetapi ternyata cukup praktis. Tentu saja ada nilai dalam parameter tipe yang dibatasi, itulah sebabnya kami melakukan percakapan ini - tetapi saya berpendapat bahwa alasan kasus yang muncul di benak Anda adalah kasus yang sudah bekerja dengan cukup baik dalam basis kode Go saat ini dalam skala besar , apakah itu bukan kasus yang benar-benar mendapat manfaat dari pemeriksaan jenis tambahan yang ketat. Yang kembali ke poin Ians - masih harus dilihat, apakah ini masalah yang terwujud dalam praktik.

@merovius Jika terserah saya, semua refleksi runtime akan dilarang, karena saya tidak ingin perangkat lunak yang dikirimkan menghasilkan kesalahan pengetikan saat runtime yang dapat memengaruhi pengguna. Ini memungkinkan pengoptimalan kompiler yang lebih agresif karena Anda tidak perlu khawatir tentang model runtime yang disejajarkan dengan model statis.

Setelah berurusan dengan migrasi proyek besar dalam skala besar dari JavaScript ke TypeScript, menurut pengalaman saya, pengetikan yang ketat menjadi lebih penting semakin besar proyek dan semakin besar tim yang mengerjakannya. Ini karena Anda perlu mengandalkan antarmuka/kontrak dari sebuah blok kode tanpa harus melihat implementasinya untuk menjaga efisiensi saat bekerja dengan tim yang besar.

Selain: Tentu saja itu tergantung pada bagaimana Anda mencapai skala, saat ini saya lebih suka pendekatan API-First, dimulai dengan file JSON OpenAPI/Swagger dan kemudian menggunakan pembuatan kode untuk membangun rintisan server dan SDK klien. Dengan demikian OpenAPI sebenarnya bertindak sebagai sistem tipe Anda untuk layanan mikro.

@ianlancetaylor

Batasan seperti konvertibilitas dan penugasan dan keterbandingan dinyatakan dalam bentuk tipe

Mengingat ada begitu banyak detail dalam aturan konversi tipe Go, sangat sulit untuk menulis kontrak kustom C untuk memenuhi fungsi konversi irisan umum berikut:

func ConvertSlice(type In, Out C(In, Out)) (x []In) []Out {
    o := make([]Out, len(x))
    for i := range x {
        o[i] = Out(x[i])
    }
    return o
}

C yang sempurna harus memungkinkan konversi:

  • antara bilangan bulat, tipe numerik titik-mengambang
  • antara semua tipe numerik kompleks
  • antara dua tipe yang tipe dasarnya identik
  • dari tipe Out yang mengimplementasikan In
  • dari jenis saluran ke jenis saluran dua arah dan kedua jenis saluran memiliki jenis elemen yang identik
  • tag struct terkait, ...
  • ...

Menurut pemahaman saya, saya tidak bisa menulis kontrak seperti itu. Jadi, apakah kita memerlukan kontrak convertible bawaan?

Tidak ada cara untuk menyatakan batasan bahwa suatu tipe memiliki kumpulan bidang yang ditentukan, tetapi saya ragu itu akan sering muncul

Mengingat bahwa jenis embedding sering digunakan dalam pemrograman Go, saya pikir kebutuhannya tidak akan jarang.

@keean Itu pendapat yang valid untuk dimiliki, tapi jelas bukan itu yang memandu desain dan pengembangan Go. Untuk berpartisipasi secara konstruktif, terimalah itu dan mulailah bekerja dari tempat kita berada dan dengan asumsi bahwa setiap perkembangan bahasa harus merupakan perubahan bertahap dari status quo. Jika Anda tidak bisa, maka ada bahasa yang lebih sesuai dengan preferensi Anda dan saya merasa semua orang - Anda khususnya - akan lebih bahagia jika Anda menyumbangkan energi Anda di sana.

@merovius Saya siap menerima bahwa perubahan pada Go harus bertahap, dan menerima status quo.

Saya baru saja membalas komentar Anda sebagai bagian dari percakapan, menyetujui bahwa saya adalah penggemar mengetik. Saya menyatakan pendapat tentang refleksi runtime, saya tidak menyarankan bahwa Go harus meninggalkan refleksi runtime. Saya mengerjakan bahasa lain, menggunakan banyak bahasa dalam pekerjaan saya. Saya sedang mengembangkan (perlahan) bahasa saya sendiri, tetapi saya selalu berharap perkembangan ke bahasa lain akan membuat ini tidak perlu.

@dotaheor Saya setuju bahwa kami tidak dapat menulis kontrak umum untuk konvertibilitas hari ini. Kita harus melihat apakah itu tampaknya menjadi masalah dalam praktik.

Menanggapi @ianlancetaylor

Saya rasa belum jelas seberapa sering orang ingin fungsi parameter pada nilai konstan. Kasus yang paling jelas adalah untuk dimensi array--tetapi Anda sudah dapat melakukannya dengan melewatkan tipe array yang diinginkan sebagai argumen tipe. Selain kasus itu, apa yang sebenarnya kita peroleh dengan melewatkan const sebagai argumen waktu kompilasi daripada argumen run-time?

Dalam kasus array, hanya meneruskan tipe array (seluruh) sebagai argumen tipe tampaknya sangat membatasi, karena kontrak tidak akan dapat menguraikan dimensi array atau tipe elemen dan menerapkan batasan pada mereka. Misalnya, dapatkah kontrak yang mengambil "tipe seluruh array" memerlukan tipe elemen tipe array untuk mengimplementasikan metode tertentu?

Tetapi panggilan Anda untuk contoh yang lebih spesifik tentang bagaimana parameter generik non-tipe akan berguna diambil dengan baik, jadi saya memperluas posting blog untuk memasukkan bagian yang mencakup beberapa kelas signifikan dari contoh kasus penggunaan dan beberapa contoh spesifik masing-masing. Karena sudah beberapa hari, posting blog lagi di sini:

Apakah Hanya Tipe Parameter Generik yang Cukup untuk Go 2 Generics?

Bagian baru berjudul "Contoh Cara Generik Lebih dari Non-Jenis yang Berguna".

Sebagai ringkasan singkat, kontrak untuk operasi matriks dan vektor dapat memaksakan batasan yang sesuai pada dimensi dan tipe elemen array. Misalnya, perkalian matriks dari matriks nxm dengan matriks mxp, masing-masing direpresentasikan sebagai larik dua dimensi, dapat dengan benar membatasi jumlah baris matriks pertama agar sama dengan jumlah kolom matriks kedua, dll.

Secara lebih umum, obat generik dapat menggunakan parameter non-tipe untuk mengaktifkan konfigurasi waktu kompilasi dan spesialisasi kode dan algoritme dalam berbagai cara. Misalnya, varian generik dari math/big.Int dapat dikonfigurasi pada waktu kompilasi ke bit tertentu dengan dan/atau penandatanganan, memenuhi permintaan untuk bilangan bulat 128-bit dan bilangan bulat lebar tetap non-asli lainnya dengan efisiensi yang wajar kemungkinan jauh lebih baik dari big.Int yang ada di mana semuanya dinamis. Varian generik dari big.Float mungkin juga dapat dispesialisasikan pada waktu kompilasi ke presisi tertentu dan/atau parameter waktu kompilasi lainnya, misalnya, untuk menyediakan implementasi generik yang cukup efisien dari format binary16, binary128, dan binary256 dari IEEE 754-2008 bahwa Go tidak mendukung secara asli. Banyak algoritme pustaka yang dapat mengoptimalkan operasinya berdasarkan pengetahuan tentang kebutuhan pengguna atau aspek tertentu dari data yang sedang diproses - misalnya, pengoptimalan algoritme grafik yang hanya berfungsi pada bobot tepi non-negatif atau hanya pada DAG atau pohon, atau pengoptimalan pemrosesan matriks yang mengandalkan matriks yang berupa segitiga atas atau bawah, atau bilangan bulat besar untuk kriptografi terkadang perlu diimplementasikan dalam waktu konstan dan terkadang tidak - dapat menggunakan generik untuk membuat diri mereka dapat dikonfigurasi pada waktu kompilasi untuk bergantung pada informasi deklaratif opsional seperti ini, sambil memastikan bahwa semua pengujian opsi waktu kompilasi ini dalam implementasi biasanya dikompilasi melalui propagasi konstan.

@bford menulis:

yaitu bahwa parameter ke obat generik terikat ke konstanta pada waktu kompilasi.

Ini adalah poin yang saya tidak mengerti. Mengapa Anda membutuhkan kondisi ini.
Secara teoritis, seseorang dapat mendefinisikan kembali variabel/parameter dalam tubuh. Tidak masalah.
Secara intuitif, saya berasumsi apakah Anda ingin menyatakan aplikasi fungsi pertama harus terjadi pada waktu kompilasi.

Tetapi untuk persyaratan ini, kata kunci seperti comp atau comptime akan lebih cocok.
Selanjutnya, jika tata bahasa golang hanya mengizinkan paling banyak dua tupel parameter untuk suatu fungsi, maka anotasi kata kunci ini dapat diabaikan karena tupel parameter pertama dari suatu jenis dan fungsi (dalam hal dua tupel parameter) akan selalu dievaluasi pada waktu kompilasi.

Poin lain: Bagaimana jika const diperluas untuk memungkinkan ekspresi runtime (true single sign on)?

Pada metode Pointer vs nilai :

Jika suatu metode tercantum dalam kontrak dengan T daripada *T , maka metode tersebut dapat berupa metode penunjuk atau metode nilai T . Untuk menghindari kekhawatiran tentang perbedaan ini, di badan fungsi generik semua panggilan metode akan menjadi panggilan metode pointer. ...

Bagaimana kuadrat ini dengan implementasi antarmuka? Jika T memiliki beberapa metode penunjuk (seperti MyInt dalam contoh), dapatkah T ditetapkan ke antarmuka dengan metode tersebut ( Stringer dalam contoh)?

Mengizinkannya berarti memiliki operasi alamat tersembunyi lainnya & , tidak mengizinkannya berarti kontrak dan antarmuka hanya dapat berinteraksi melalui sakelar tipe eksplisit. Tidak ada solusi yang tampak baik bagi saya.

(Catatan: kita harus meninjau kembali keputusan ini jika itu mengarah pada kebingungan atau kode yang salah.)

Saya melihat tim sudah memiliki beberapa keberatan tentang ambiguitas ini dalam sintaks metode pointer. Saya hanya menambahkan bahwa ambiguitas juga mempengaruhi implementasi antarmuka (dan secara implisit menambahkan keberatan saya tentang hal itu juga).

@fJavierZunzunegui Anda benar, teks saat ini menyiratkan bahwa ketika menetapkan nilai parameter tipe ke tipe antarmuka, operasi alamat implisit mungkin diperlukan. Itu mungkin alasan lain untuk tidak menggunakan alamat implisit saat menjalankan metode. Kita harus melihat.

Pada tipe Parameterized , khususnya mengenai parameter tipe yang disematkan sebagai bidang dalam struct:

Mempertimbangkan

type Lockable(type T) struct {
    T
    sync.Locker
}

Bagaimana jika T memiliki metode bernama Lock atau Unlock ? Struktur tidak akan dikompilasi. Kondisi tidak memiliki metode X ini tidak didukung oleh kontrak, oleh karena itu kami memiliki kode yang tidak valid yang tidak melanggar kontrak (mengalahkan seluruh tujuan kontrak).

Itu menjadi lebih rumit jika Anda memiliki beberapa parameter yang disematkan (misalnya T1 dan T2 ) karena itu tidak boleh memiliki metode umum (sekali lagi, tidak ditegakkan oleh kontrak). Selain itu, mendukung metode arbitrer yang bergantung pada tipe yang disematkan berkontribusi pada pembatasan waktu kompilasi yang sangat terbatas pada sakelar tipe untuk struct tersebut (sangat mirip dengan Type assertions dan switch ).

Seperti yang saya lihat, ada 2 alternatif bagus:

  • melarang penyematan parameter tipe sama sekali: sederhana, tetapi dengan biaya kecil (jika metode ini diperlukan, seseorang harus menulisnya secara eksplisit di struct dengan bidang).
  • batasi metode yang dapat dipanggil ke metode kontrak: mirip dengan menyematkan antarmuka. Ini menyimpang dari go normal (non-tujuan) tetapi tanpa biaya (metode tidak perlu ditulis secara eksplisit dalam struct dengan bidang).

Struktur tidak akan dikompilasi.

Itu akan dikompilasi. Cobalah. Apa yang gagal dikompilasi adalah panggilan ke metode ambigu. Namun, poin Anda masih valid.

Solusi kedua Anda, membatasi metode yang dapat dipanggil ke yang disebutkan dalam kontrak, tidak akan berfungsi: bahkan jika kontrak pada T ditentukan Lock dan Unlock , Anda masih bisa' t panggil mereka dengan Lockable .

@jba terima kasih atas wawasan tentang kompilasi.

Dengan solusi kedua yang saya maksud adalah memperlakukan parameter tipe yang disematkan seperti yang kita lakukan dengan antarmuka sekarang, sehingga jika metode tersebut tidak ada dalam kontrak, metode tersebut tidak segera dapat diakses setelah penyematan. Dalam skenario ini, karena T tidak memiliki kontrak, ia diperlakukan secara efektif sebagai interface{} , sehingga tidak akan bertentangan dengan sync.Locker bahkan jika T digunakan dengan jenis dengan metode tersebut. Ini mungkin bisa membantu menjelaskan maksud saya .

Either way saya lebih suka solusi pertama (melarang embedding sama sekali), jadi jika itu adalah preferensi Anda, ada sedikit tujuan membahas yang kedua! :senyum:

Contoh yang diberikan oleh @JavierZunzunegui juga mencakup kasus lain. Bagaimana jika T adalah struct yang memiliki bidang noCopy noCopy ? Kompiler harus dapat menangani kasus itu juga.

Tidak yakin apakah ini tempat yang tepat untuk ini, tetapi saya ingin berkomentar dengan kasus penggunaan dunia nyata yang konkret untuk tipe generik yang memungkinkan "parameterisasi pada nilai non-tipe seperti konstanta", dan khusus untuk kasus array . Saya harap ini bermanfaat.

Di dunia saya tanpa obat generik, saya menulis banyak kode yang terlihat seperti ini:

import "math/bits"

// SigEl is the element type used in variable length bit vectors, 
// can be any unsigned integer type
type SigEl = uint

// SigElBits is the number of bits storable in each SigEl
const SigElBits = 8 << uint((^SigEl(0)>>32&1)+(^SigEl(0)>>16&1)+(^SigEl(0)>>8&1))

// HammingDist counts the number bitwise differences between two
// bit vectors b1 and b2. I want this to be generic
// Function will panic at runtime if b1 and b2 aren't of equal length.
func HammingDist(b1, b2 []SigEl) (sum int) {
    // Give the compiler a hint so it won't need to bounds check the slices in loops
    _ = b1[len(b2)-1]  
        // This switch is optimized away because SigElBits is const
    switch SigElBits {   // Yay no golang generics!
    case 64:
        _ = b2[len(b1)-1]
        for x := range b1 {
            sum += bits.OnesCount64(uint64(b1[x] ^ b2[x]))
        }
    case 32:
        _ = b2[len(b1)-1]
        for x := range b1 {
            sum += bits.OnesCount32(uint32(b1[x] ^ b2[x]))
        }
    case 16:
        _ = b2[len(b1)-1]
        for x := range b1 {
            sum += bits.OnesCount16(uint16(b1[x] ^ b2[x]))
        }
    case 8:
        _ = b2[len(b1)-1]
        for x := range b1 {
            sum += bits.OnesCount8(uint8(b1[x] ^ b2[x]))
        }
    }
    return sum
}

Ini bekerja cukup baik, dengan satu kerutan. Saya sering membutuhkan ratusan juta []SigEl s, dan panjangnya seringkali 128-384 total bit. Karena irisan memaksakan overhead 192-bit tetap di atas ukuran larik yang mendasarinya, ketika larik itu sendiri adalah 384 bit atau kurang, ini membebankan overhead memori 50-150% yang tidak perlu, yang jelas mengerikan.

Solusi saya adalah mengalokasikan sepotong Sig _arrays_, dan kemudian mengirisnya dengan cepat sebagai parameter ke HammingDist di atas:

const SigBits = 256  // Any multiple of SigElBits is valid

// Sig is the bit vector array type
type Sig [SigBits/SigElBits]SigEl

bitVects := make([]Sig, 100000000)
// stuff happens ... 

// Note slicing below, just to make the arrays "generic" for the call 
dist := HammingDist(bitVects[x][:], bitVects[y][:])

Apa yang saya _suka_ dapat lakukan alih-alih semua itu adalah mendefinisikan jenis Tanda Tangan generik dan menulis ulang semua hal di atas sebagai (seperti):

contract UnsignedInteger(T) {
    T uint, uint8, uint16, uint32, uint64
}

type Signature (type Element UnsignedInteger, n int) [n]Element

// HammingDist counts the number bitwise differences between two bit vectors
func HammingDist(b1, b2 *Signature) (sum int) {
    for x := range *b1 {
        // Assuming the std lib bits.OnesCount becomes generic over 
        // all UnsignedInteger types
        sum += bits.OnesCount(*b1[x] ^ *b2[x])
    }
    return sum
}

Jadi untuk menggunakan perpustakaan ini:

type sigEl = uint   // Any unsigned int type
const sigElBits = 8 << uint((^SigEl(0)>>32&1)+(^SigEl(0)>>16&1)+(^SigEl(0)>>8&1))
const sigBits = 256  // Any multiple of SigElBits is valid
type sig Signature(sigEl, sigBits/sigElBits)

bitVects := make([]sig, 100000000)
// stuff happens ... 

dist := HammingDist(&bitVects[x], &bitVects[y])

Seorang insinyur dapat bermimpi...

Jika Anda tahu seberapa besar panjang bit maksimum, Anda dapat menggunakan sesuatu seperti ini sebagai gantinya:

contract uintArrayOfFixedLength(ElemType,ArrayType)
{
    ArrayType [1]ElemType,[2]ElemType,...,[maxBit]ElemType
    ElemType uint8,uint16,uint32,uint64
}

func HammingDist(type ElemType,ArrayType uintArrayOfFixedLength)(t1,t2 ArrayType) (sum int)
{

}

@vsivsi Saya tidak yakin saya mengerti bagaimana menurut Anda itu akan memperbaiki keadaan - apakah Anda mungkin berasumsi, bahwa kompiler akan menghasilkan versi instantiated dari fungsi itu untuk setiap panjang array yang mungkin? Karena ISTM bahwa a) itu tidak terlalu mungkin, jadi b) Anda akan berakhir dengan karakteristik kinerja yang sama persis seperti yang Anda lakukan sekarang. Implementasi yang paling mungkin, IMO, adalah bahwa kompiler meneruskan panjang dan pointer ke elemen pertama, jadi Anda masih akan secara efektif meneruskan sepotong, dalam kode yang dihasilkan (maksud saya, Anda tidak akan melewatkan kapasitas, tapi saya tidak berpikir kata tambahan di tumpukan benar-benar penting).

Sejujurnya, IMO apa yang Anda katakan adalah contoh yang cukup bagus untuk menggunakan obat generik secara berlebihan, di mana mereka tidak diperlukan - "array dengan panjang tak tentu" adalah persis untuk apa irisan itu.

@Merovius Terima kasih, saya pikir komentar Anda mengungkapkan beberapa poin diskusi yang menarik.

"array dengan panjang tak tentu" adalah persis untuk apa irisan itu.

Benar, tetapi dalam contoh saya tidak ada array dengan panjang tak tentu. Panjang larik adalah konstanta yang diketahui pada _waktu kompilasi_. Inilah tepatnya gunanya array, tetapi mereka kurang digunakan di golang IMO karena sangat tidak fleksibel.

Untuk lebih jelasnya, saya tidak menyarankan

type Signature (type Element UnsignedInteger, n int) [n]Element

berarti n adalah variabel runtime. Itu harus tetap konstan dalam arti yang sama seperti hari ini:

const n = 10
type nArray [n]uint               // works
type nSigInt Signature(uint, n)   // works 

var m = int(n)
type mArray [m]uint               // error
type mSigInt Signature(uint, m)   // error 

Jadi mari kita lihat "biaya" dari fungsi HammingDist berbasis irisan. Saya setuju bahwa perbedaan antara melewatkan array sebagai bitVects[x][:] vs &bitVects[x] kecil(-ish, faktor 3 maks). Perbedaan nyata ada pada kode dan pemeriksaan runtime yang perlu terjadi di dalam fungsi itu.

Dalam versi berbasis irisan, kode runtime perlu memeriksa akses irisan untuk memastikan keamanan memori. Ini berarti bahwa versi kode ini dapat panik (atau mekanisme pengecekan dan pengembalian kesalahan eksplisit diperlukan untuk mencegahnya). Penetapan NOP ( _ = b1[len(b2)-1] ) membuat perbedaan kinerja yang berarti dengan memberikan petunjuk kepada pengoptimal kompiler bahwa ia tidak perlu memeriksa setiap akses irisan dalam loop. Tetapi pemeriksaan batas minimal ini masih diperlukan, meskipun larik dasar yang diteruskan selalu sama panjangnya. Selain itu, kompiler mungkin mengalami kesulitan dalam mengoptimalkan loop for/range (misalnya melalui unrolling ).

Sebaliknya, versi fungsi berbasis array generik tidak dapat panik saat runtime (tidak memerlukan penanganan kesalahan) dan mengabaikan kebutuhan akan logika pemeriksaan batas bersyarat. Saya sangat meragukan versi generik yang dikompilasi dari fungsi tersebut perlu "melewati" panjang array seperti yang Anda sarankan karena secara harfiah merupakan nilai konstan yang merupakan bagian dari tipe yang dipakai pada waktu kompilasi.

Selanjutnya, untuk dimensi array kecil (penting dalam kasus saya), akan mudah bagi kompiler untuk membuka gulungan secara menguntungkan atau bahkan sepenuhnya mengoptimalkan loop for/range untuk mendapatkan kinerja yang layak, karena ia akan mengetahui pada waktu kompilasi apa dimensi tersebut. .

Manfaat besar lainnya dari versi generik kode ini adalah memungkinkan pengguna modul HammingDist untuk menentukan tipe int yang tidak ditandatangani dalam kode mereka sendiri. Versi non-generik memerlukan modul itu sendiri untuk dimodifikasi untuk mengubah tipe yang ditentukan SigEl , karena tidak ada cara untuk "meneruskan" tipe ke modul. Konsekuensi dari perbedaan ini adalah implementasi fungsi jarak menjadi lebih sederhana ketika tidak perlu menulis kode terpisah untuk setiap kasus uint {8,16,32,64}-bit.

Biaya versi fungsi berbasis irisan dan kebutuhan untuk memodifikasi kode pustaka untuk menyetel tipe elemen adalah konsesi yang sangat suboptimal yang diperlukan untuk menghindari keharusan mengimplementasikan dan memelihara versi "NxM" dari fungsi ini. Dukungan umum untuk tipe array berparameter (konstan) akan menyelesaikan masalah ini:

// With generics + parameterized constant array lengths:
type Signature (type Element UnsignedInteger, n int) [n]Element
func HammingDist(b1, b2 *Signature) (sum int) { ... }

// Without generics
func HammingDistL1Uint(b1, b2 [1]uint) (sum int) { ... }
func HammingDistL1Uint8(b1, b2 [1]uint8) (sum int) { ... }
func HammingDistL1Uint16(b1, b2 [1]uint16) (sum int) { ... }
func HammingDistL1Uint32(b1, b2 [1]uint32) (sum int) { ... }
func HammingDistL1Uint64(b1, b2 [1]uint64) (sum int) { ... }

func HammingDistL2Uint(b1, b2 [2]uint) (sum int) { ... }
func HammingDistL2Uint8(b1, b2 [2]uint8) (sum int) { ... }
func HammingDistL2Uint16(b1, b2 [2]uint16) (sum int) { ... }
func HammingDistL2Uint32(b1, b2 [2]uint32) (sum int) { ... }
func HammingDistL2Uint64(b1, b2 [2]uint64) (sum int) { ... }

func HammingDistL3Uint(b1, b2 [3]uint) (sum int) { ... }
func HammingDistL3Uint8(b1, b2 [3]uint8) (sum int) { ... }
func HammingDistL3Uint16(b1, b2 [3]uint16) (sum int) { ... }
func HammingDistL3Uint32(b1, b2 [3]uint32) (sum int) { ... }
func HammingDistL3Uint64(b1, b2 [3]uint64) (sum int) { ... }

// and L4, L5, L6 ... ad nauseum

Menghindari mimpi buruk di atas, atau biaya yang sangat nyata dari alternatif saat ini sepertinya _berlawanan_ dari "penggunaan berlebihan generik" bagi saya. Saya setuju dengan @sighoya bahwa menghitung semua panjang array yang diizinkan dalam kontrak dapat bekerja untuk serangkaian kasus yang sangat terbatas, tetapi saya percaya itu terlalu terbatas bahkan untuk kasus saya, karena bahkan jika saya menempatkan batas atas dukungan di a rendah total 384 bit, yang akan membutuhkan hampir 50 istilah dalam klausa ArrayType [1]ElemType,[2]ElemType,...,[maxBit]ElemType kontrak untuk mencakup kasus uint8 .

Benar, tetapi dalam contoh saya tidak ada array dengan panjang tak tentu. Panjang array adalah konstanta yang diketahui pada waktu kompilasi.

Saya mengerti itu, tetapi perhatikan bahwa saya juga tidak mengatakan "saat runtime". Anda ingin menulis kode yang tidak memperhatikan panjang array. Slice sudah bisa melakukan itu.

Saya sangat meragukan versi generik yang dikompilasi dari fungsi tersebut perlu "melewati" panjang array seperti yang Anda sarankan karena secara harfiah merupakan nilai konstan yang merupakan bagian dari tipe yang dipakai pada waktu kompilasi.

Versi generik dari fungsi akan - karena setiap instantiasi dari tipe itu menggunakan konstanta yang berbeda. Itulah mengapa saya mendapat kesan bahwa Anda berasumsi bahwa kode yang dihasilkan tidak akan umum, tetapi diperluas untuk setiap jenis. yaitu Anda tampaknya berasumsi bahwa akan ada beberapa instantiasi dari fungsi yang dihasilkan, untuk [1]Element , [2]Element , dll. Saya katakan, bahwa itu tampaknya tidak mungkin bagi saya, tampaknya lebih mungkin bahwa akan ada satu versi yang dihasilkan, yang pada dasarnya setara dengan versi irisan.

Tentu saja tidak harus seperti itu. Jadi, ya, Anda benar bahwa Anda tidak perlu melewatkan panjang array. Saya hanya memprediksi dengan kuat bahwa itu akan diterapkan seperti itu dan tampaknya asumsi yang dipertanyakan bahwa itu tidak akan terjadi. (FWIW, saya juga berpendapat bahwa jika Anda ingin kompiler menghasilkan badan fungsi khusus untuk panjang yang terpisah, itu bisa juga melakukannya secara transparan untuk irisan juga, tapi itu diskusi yang berbeda).

Manfaat besar lainnya dari versi generik kode

Untuk memperjelas: Dengan "versi generik", apakah Anda mengacu pada gagasan umum obat generik, seperti yang diterapkan misalnya dalam draf desain kontrak saat ini, atau apakah Anda merujuk lebih khusus ke obat generik dengan parameter non-tipe? Karena kelebihan yang Anda sebutkan dalam paragraf ini juga berlaku untuk rancangan desain kontrak saat ini.

Saya tidak mencoba membuat kasus melawan obat generik secara umum di sini. Saya hanya menjelaskan mengapa saya tidak berpikir contoh Anda berfungsi untuk menunjukkan bahwa kita membutuhkan jenis parameter selain jenis.

// With generics + parameterized constant array lengths:
// Without generics

Ini adalah dikotomi yang salah (dan sangat jelas sehingga saya agak frustrasi dengan Anda). Ada juga "dengan parameter tipe, tetapi tanpa parameter bilangan bulat":

contract Unsigned(T) {
    T uint, uint8, uint16, uint32, uint64
}
func HammingDist(type T Unsigned) (b1, b2 []T) (sum int) {
    if len(b1) != len(b2) {
        panic("slices of different lengths passed to HammingDist")
    }
    for i := range b1 {
        sum += bits.OnesCount(b1[i]^b2[i]) // Same assumption about OnesCount being generic you made above
    }
    return sum
}

Yang tampaknya baik-baik saja bagi saya. Ini sedikit kurang aman untuk tipe, dengan membutuhkan runtime-panic jika tipenya tidak cocok. Tapi, dan itu semacam maksud saya, itulah satu-satunya keuntungan menambahkan parameter generik non-tipe dalam contoh Anda (dan itu adalah keuntungan yang sudah jelas, IMO). Keuntungan kinerja yang Anda prediksi bergantung pada asumsi yang cukup kuat tentang bagaimana generik secara umum dan generik di atas parameter non-tipe secara khusus diimplementasikan. Bahwa saya, secara pribadi, tidak mempertimbangkan kemungkinan besar berdasarkan apa yang saya dengar dari tim Go sejauh ini.

Saya sangat meragukan versi generik yang dikompilasi dari fungsi tersebut perlu "melewati" panjang array seperti yang Anda sarankan karena secara harfiah merupakan nilai konstan yang merupakan bagian dari tipe yang dipakai pada waktu kompilasi.

Anda hanya berasumsi bahwa obat generik akan berfungsi seperti templat C++ dan implementasi fungsi duplikat, tetapi itu tidak benar. Proposal secara eksplisit mengizinkan implementasi tunggal dengan parameter tersembunyi.

Saya pikir jika Anda benar-benar membutuhkan kode templat untuk sejumlah kecil tipe numerik, tidak terlalu membebani untuk menggunakan pembuat kode. Generik benar-benar hanya sepadan dengan kompleksitas kode untuk hal-hal seperti tipe wadah di mana ada manfaat kinerja yang terukur dari menggunakan tipe primitif tetapi Anda tidak dapat berharap untuk hanya menghasilkan sejumlah kecil templat kode terlebih dahulu.

Saya jelas tidak tahu bagaimana pengelola golang pada akhirnya akan menerapkan apa pun, jadi saya akan menahan diri untuk tidak berspekulasi lebih jauh dan dengan senang hati tunduk pada mereka yang memiliki lebih banyak pengetahuan orang dalam.

Yang saya tahu adalah bahwa untuk contoh masalah dunia nyata yang saya bagikan di atas, perbedaan kinerja potensial antara implementasi berbasis irisan saat ini dan yang berbasis array generik yang dioptimalkan dengan baik adalah substansial.

BenchmarkHD/256-bit_unrolled_array_HD-20            2000000000           1.05 ns/op        0 B/op          0 allocs/op
BenchmarkHD/256-bit_slice_HD-20                     300000000            5.10 ns/op        0 B/op          0 allocs/op

Kode di: https://github.com/vsivsi/hdtest

Itu adalah perbedaan kinerja potensial 5 kali lipat untuk kasing 4x64-bit (titik manis dalam pekerjaan saya) hanya dengan sedikit pembukaan gulungan (dan pada dasarnya tidak ada kode tambahan yang dipancarkan) dalam kasing larik. Perhitungan ini ada di loop dalam algoritme saya, dibuat bertriliun-triliun kali, jadi perbedaan kinerja 5x cukup besar. Tetapi untuk mewujudkan peningkatan efisiensi ini hari ini, saya perlu menulis setiap versi fungsi, untuk setiap jenis elemen dan panjang larik yang diperlukan.

Tapi ya, jika pengoptimalan seperti ini tidak pernah diterapkan oleh pengelola, maka seluruh latihan menambahkan panjang larik berparameter ke obat generik tidak akan ada gunanya, setidaknya karena mungkin bermanfaat untuk kasus contoh ini.

Pokoknya diskusi yang menarik. Saya tahu ini adalah masalah yang kontroversial, jadi terima kasih telah menjaganya tetap sopan!

@vsivsi FWIW, kemenangan yang Anda amati lenyap jika Anda tidak membuka gulungan loop Anda secara manual (atau jika Anda juga membuka gulungan loop di atas sepotong) - jadi ini masih tidak benar-benar mendukung argumen Anda bahwa parameter-integer membantu karena mereka mengizinkan compiler untuk melakukan unrolling untuk Anda. Tampaknya sains yang buruk bagi saya, untuk memperdebatkan X atas Y, berdasarkan pada kompiler yang menjadi pintar secara sewenang-wenang untuk X dan tetap bodoh secara sewenang-wenang untuk Y. Tidak jelas bagi saya, mengapa heuristik terbuka yang berbeda akan memicu dalam kasus perulangan array , tetapi tidak memicu dalam kasus perulangan irisan dengan panjang yang diketahui pada waktu kompilasi. Anda tidak menunjukkan manfaat dari rasa obat generik tertentu di atas yang lain, Anda menunjukkan manfaat dari heuristik terbuka yang berbeda itu.

Tetapi bagaimanapun juga, tidak ada yang benar-benar berpendapat bahwa menghasilkan kode khusus untuk setiap instantiasi fungsi generik tidak akan berpotensi lebih cepat - hanya saja ada pengorbanan lain yang perlu dipertimbangkan ketika memutuskan apakah Anda ingin melakukannya.

@Merovius Saya pikir kasus terkuat untuk obat generik dalam contoh semacam ini adalah dengan elaborasi waktu kompilasi (jadi memancarkan fungsi unik untuk setiap tipe-level-integer) di mana kode yang akan dispesialisasikan ada di perpustakaan. Jika pengguna perpustakaan akan menggunakan sejumlah fungsi yang terbatas, maka mereka mendapatkan keuntungan dari versi yang dioptimalkan. Jadi jika kode saya hanya menggunakan array dengan panjang 64, saya dapat menggunakan elaborasi yang dioptimalkan dari fungsi perpustakaan untuk panjang 64.

Dalam kasus khusus ini, itu tergantung pada distribusi frekuensi panjang larik, karena kita mungkin tidak ingin menguraikan semua fungsi yang mungkin jika ada ribuan karena keterbatasan memori, dan pembuangan cache halaman yang dapat membuat segalanya lebih lambat. Jika misalnya ukuran kecil adalah umum, tetapi yang lebih besar dimungkinkan (ukuran distribusi ekor panjang) maka kita dapat menguraikan fungsi khusus untuk bilangan bulat kecil dengan loop yang tidak digulung (katakanlah 1 hingga 64) dan kemudian menyediakan satu versi umum dengan tersembunyi -parameter untuk sisanya.

Saya tidak suka gagasan "kompiler pintar yang sewenang-wenang" dan berpikir ini adalah argumen yang buruk. Berapa lama saya harus menunggu kompiler pintar yang sewenang-wenang ini? Saya sangat tidak menyukai gagasan kompiler yang mengubah tipe, misalnya mengoptimalkan irisan ke Array membuat spesialisasi tersembunyi dalam bahasa dengan refleksi, seperti ketika Anda merenungkan irisan itu sesuatu yang tidak terduga bisa terjadi.

Mengenai "dilema umum", secara pribadi saya akan memilih "membuat kompiler lebih lambat/melakukan lebih banyak pekerjaan", tetapi coba dan buat secepat mungkin dengan menggunakan implementasi yang baik dan kompilasi terpisah. Rust tampaknya bekerja dengan cukup baik, dan setelah pengumuman Intel baru-baru ini sepertinya akhirnya bisa menggantikan 'C' sebagai bahasa pemrograman sistem utama. Waktu kompilasi tampaknya tidak menjadi faktor dalam keputusan Intel, karena memori runtime dan keamanan konkurensi dengan kecepatan seperti 'C' tampaknya menjadi faktor kunci. "Sifat" Rust adalah implementasi yang wajar dari kelas tipe generik, mereka memiliki beberapa kasus sudut yang mengganggu yang menurut saya berasal dari desain sistem tipe mereka.

Merujuk kembali ke diskusi kita sebelumnya, saya harus berhati-hati untuk memisahkan diskusi tentang obat generik secara umum, dan bagaimana mereka mungkin secara khusus berlaku untuk Go. Karena itu saya tidak yakin Go harus memiliki obat generik karena memperumit bahasa yang sederhana dan elegan, seperti halnya 'C' tidak memiliki obat generik. Saya masih berpikir ada celah di pasar untuk bahasa yang memiliki implementasi umum sebagai fitur inti, tetapi tetap sederhana dan elegan.

Saya ingin tahu apakah ada kemajuan dalam hal ini.

Berapa lama saya bisa mencoba obat generik. Aku sudah lama menunggu

@Nsgj Anda dapat memeriksa CL ini: https://go-review.googlesource.com/c/go/+/187317/

Dalam spek saat ini, apakah ini mungkin?

contract Point(T) {
  T struct { X, Y float64 }
}

Dengan kata lain, tipenya harus berupa struct dengan dua bidang, X dan Y, bertipe float64.

edit: dengan contoh penggunaan

func generate(type T Point)() T {
  return T{X: randomFloat64(), Y: randomFloat64()}
}

@abuchanan-nr Ya, rancangan desain saat ini akan mengizinkannya, meskipun sulit untuk melihat bagaimana itu akan berguna.

Saya juga tidak yakin itu berguna, tetapi saya tidak melihat contoh yang jelas tentang penggunaan tipe struct khusus dalam daftar tipe kontrak. Sebagian besar contoh menggunakan tipe bawaan.

FWIW, saya membayangkan perpustakaan grafis 2D. Anda mungkin ingin setiap simpul memiliki sejumlah bidang khusus aplikasi, seperti warna, gaya, dll. Tetapi Anda mungkin juga menginginkan pustaka umum metode dan algoritme hanya untuk bagian geometri, yang hanya benar-benar bergantung pada koordinat X,Y. Mungkin bagus untuk meneruskan tipe simpul khusus Anda ke perpustakaan ini, misalnya

type MyVertex struct {
  X, Y float64
  Color color.Color
  OtherAttr int
}
p := geo.RandomPolygon(MyVertex)()

for _, vert := range p.Vertices() {
  p.Color = randColor()
}

Sekali lagi, tidak yakin itu ternyata menjadi desain yang bagus dalam praktiknya, tetapi di situlah imajinasi saya saat itu :)

Lihat https://godoc.org/image#Image untuk mengetahui cara melakukannya di Go standar hari ini.

Sehubungan dengan Operator/Jenis dalam kontrak :

Ini menghasilkan duplikasi dari banyak metode umum, karena kita membutuhkannya dalam format operator ( + , == , < , ...) dan format metode ( Plus(T) T , Equal(T) bool , LessThan(T) bool , ...).

Saya mengusulkan kita menyatukan dua pendekatan ini menjadi satu, format metode. Untuk mencapai itu, tipe yang dideklarasikan sebelumnya ( int , int64 , string , ...) perlu dilemparkan ke tipe dengan metode arbitrer. Untuk kasus sederhana (sepele) yang sudah mungkin ( type MyInt int; func (i MyInt) LessThan(o MyInt) bool {return int(i) < int(o)} ), tetapi nilai sebenarnya terletak pada tipe komposit ( []int -> []MyInt , map[int]struct{} -> map[MyInt]struct{} , dan seterusnya untuk saluran, penunjuk, ...), yang tidak diperbolehkan (lihat FAQ ). Mengizinkan konversi ini adalah perubahan signifikan yang berjalan dengan sendirinya, jadi saya telah memperluas teknis dalam Proposal Konversi Tipe Santai . Itu akan memungkinkan fungsi generik untuk tidak berurusan dengan operator dan masih mendukung semua jenis, termasuk yang telah dideklarasikan sebelumnya.

Perhatikan bahwa perubahan ini juga menguntungkan tipe yang tidak dideklarasikan sebelumnya. Di bawah proposal saat ini, diberikan type X struct{S string} (yang berasal dari perpustakaan eksternal, sehingga Anda tidak dapat menambahkan metode ke dalamnya), katakanlah Anda memiliki []X dan ingin meneruskannya ke fungsi umum mengharapkan []T , untuk T memenuhi kontrak Stringer . Itu akan membutuhkan type X2 X; func(x X2) String() string {return x.S} , dan salinan dalam []X ke []X2 . Di bawah usulan perubahan pada proposal ini, Anda menyimpan salinan dalam seluruhnya.

CATATAN: Proposal Konversi Tipe Santai yang disebutkan membutuhkan tantangan.

@JavierZunzunegui Menyediakan "format metode" (atau format operator) untuk operator unary/biner dasar bukanlah masalah. Cukup mudah untuk memperkenalkan metode seperti +(x int) int hanya dengan mengizinkan simbol operator sebagai nama metode, dan untuk memperluasnya ke tipe bawaan (meskipun ini pun rusak untuk shift karena operator tangan kanan dapat tipe integer arbitrer - kami tidak memiliki cara untuk mengekspresikannya saat ini). Masalahnya adalah itu tidak cukup. Salah satu hal yang perlu diekspresikan oleh kontrak adalah apakah nilai x tipe X dapat dikonversi ke tipe parameter tipe T seperti pada T(x) (dan sebaliknya). Artinya, seseorang perlu menciptakan "format metode" untuk konversi yang diizinkan. Selanjutnya, perlu ada cara untuk menyatakan bahwa konstanta yang tidak diketik c dapat ditetapkan ke (atau dikonversi ke) variabel tipe parameter tipe T : apakah sah untuk menetapkan, katakanlah, 256 hingga t dari tipe T ? Bagaimana jika T adalah byte ? Ada beberapa hal lagi seperti ini. Seseorang dapat menemukan notasi "format metode" untuk hal-hal ini, tetapi itu menjadi rumit dengan cepat, dan tidak jelas itu lebih mudah dimengerti atau dibaca.

Saya tidak mengatakan itu tidak bisa dilakukan, tetapi kami belum menemukan pendekatan yang memuaskan dan jelas. Draf desain saat ini yang hanya menyebutkan jenis di sisi lain cukup mudah untuk dipahami.

@griesemer Ini mungkin sulit di Go karena prioritas lain, tetapi ini adalah masalah yang diselesaikan dengan baik secara umum. Ini adalah salah satu alasan saya melihat konversi implisit sebagai buruk. Ada alasan lain seperti kejadian ajaib yang tidak terlihat oleh seseorang yang membaca kode.

Jika tidak ada konversi implisit dalam sistem tipe, maka saya dapat menggunakan kelebihan beban untuk secara tepat mengontrol rentang jenis yang diterima, dan Antarmuka mengontrol kelebihan beban.

Saya akan cenderung untuk mengungkapkan kesamaan antara tipe menggunakan antarmuka, maka operasi seperti '+' akan diekspresikan secara umum sebagai operasi pada antarmuka numerik daripada tipe. Anda harus memiliki variabel tipe serta antarmuka untuk mengekspresikan batasan bahwa argumen dan hasil penjumlahan harus bertipe sama.

Jadi di sini operator tambahan dinyatakan beroperasi di atas tipe dengan antarmuka Numerik. Ini terkait dengan baik dengan matematika, di mana 'bilangan bulat' dan 'penjumlahan' membentuk "grup" misalnya.

Anda akan berakhir dengan sesuatu seperti:

+(T Addable)(x T, y T) T

Jika Anda mengizinkan pemilihan antarmuka implisit, maka operator '+' bisa saja menjadi metode antarmuka Numerik, tetapi saya pikir itu akan menyebabkan masalah dengan pemilihan metode di Go?

@griesemer pada poin Anda tentang konversi:

Salah satu hal yang perlu diungkapkan oleh kontrak adalah apakah nilai x tipe X dapat dikonversi ke tipe parameter tipe T seperti pada T(x) (dan sebaliknya). Artinya, seseorang perlu menemukan "format metode" untuk konversi yang diizinkan

Saya dapat melihat bagaimana itu akan menjadi komplikasi, tetapi saya pikir itu tidak diperlukan. Menurut saya, konversi seperti itu akan terjadi di luar kode umum, oleh penelepon. Contoh (menggunakan Stringify sesuai desain draf):

Stringify(int)([]int{1,2}) // does not compile
type MyInt int
func (i MyInt) String() string {...}
Stringify(MyInt)([]MyInt([]int{1,2})) // OK. Generic type MyInt could be inferred

Di atas, sejauh menyangkut Stringify argumennya adalah tipe []MyInt dan memenuhi kontrak. Kode generik tidak dapat mengonversi tipe generik menjadi apa pun (selain antarmuka yang mereka terapkan, sesuai kontrak), justru karena kontrak mereka tidak menyatakan apa pun tentang itu.

@JavierZunzunegui Saya tidak melihat bagaimana penelepon dapat melakukan konversi seperti itu tanpa mengeksposnya di antarmuka/kontrak. Misalnya, saya mungkin ingin menerapkan algoritma numerik generik (fungsi parameter) yang beroperasi pada berbagai tipe integer atau floating point. Sebagai bagian dari algoritme itu, kode fungsi perlu menetapkan nilai konstanta c1 , c2 , dll. ke nilai tipe parameter T . Saya tidak melihat bagaimana kode dapat melakukan ini tanpa mengetahui bahwa boleh saja menetapkan konstanta ini ke variabel bertipe T . (Seseorang tentu tidak ingin melewatkan konstanta tersebut ke dalam fungsi.)

func NumericAlgorithm(type T SomeContract)(vector []T) T {
   ...
   vector[i] = 3.1415  // <<< how do we know this is valid without the contract telling us?
   ...
}

perlu menetapkan nilai konstan c1 , c2 , dll. ke nilai tipe parameter T

@griesemer Saya akan (dalam pandangan saya tentang bagaimana obat generik/seharusnya) mengatakan di atas adalah pernyataan masalah yang salah. Anda memerlukan T untuk didefinisikan sebagai float32 , tetapi kontrak hanya menyatakan metode apa yang tersedia untuk T , bukan apa yang didefinisikan. Jika Anda membutuhkan ini, Anda dapat menyimpan vector sebagai []T dan memerlukan argumen func(float32) T ( vector[i] = f(c1) ), atau lebih baik menyimpan vector sebagai []float32 dan memerlukan T oleh kontrak untuk memiliki metode DoSomething(float32) atau DoSomething([]float32) , karena saya mengasumsikan T dan mengapung harus berinteraksi di beberapa titik. Itu berarti T mungkin atau mungkin tidak didefinisikan sebagai type T float32 , yang bisa kita katakan adalah ia memiliki metode yang disyaratkan oleh kontrak.

@JavierZunzunegui Saya tidak mengatakan sama sekali bahwa T didefinisikan sebagai float32 - bisa berupa float32 , float64 , atau bahkan salah satu jenis yang kompleks. Lebih umum, jika konstanta adalah bilangan bulat, mungkin ada berbagai tipe bilangan bulat yang valid untuk diteruskan ke fungsi ini, dan beberapa yang tidak. Ini tentu bukan "pernyataan masalah yang salah". Masalahnya nyata - tentu saja tidak dibuat-buat sama sekali untuk ingin dapat menulis fungsi seperti itu - dan masalahnya tidak hilang dengan menyatakannya "salah".

@griesemer Saya mengerti, saya pikir Anda hanya peduli dengan konversi, saya tidak mendaftarkan elemen kunci yang berurusan dengan konstanta yang tidak diketik.

Anda dapat melakukan sesuai jawaban saya di atas, dengan T memiliki metode DoSomething(X) , dan fungsi mengambil argumen tambahan func(float64) X , sehingga bentuk generik didefinisikan oleh dua jenis ( T,X ). Cara Anda menjelaskan masalah X biasanya float32 atau float64 dan argumen fungsinya adalah func(f float64) float32 {return float32(f)} atau func(f float64) float64 {return f} .

Lebih penting lagi, seperti yang Anda soroti, untuk kasus bilangan bulat ada masalah bahwa format bilangan bulat yang kurang tepat mungkin tidak cukup untuk konstanta yang diberikan. Pendekatan teraman menjadi menjaga fungsi generik dua-ketik ( T,X ) pribadi dan mengekspos publik hanya MyFunc32 / MyFunc64 /etc.

Saya akan mengakui bahwa MyFunc32(int32) / MyFunc64(int64) /etc. kurang praktis daripada MyFunc(type T Numeric) tunggal (kebalikannya tidak dapat dipertahankan!). Tapi ini hanya untuk implementasi umum yang mengandalkan konstanta, dan terutama konstanta bilangan bulat - ada berapa banyak? Selebihnya, Anda mendapatkan kebebasan tambahan untuk tidak dibatasi pada beberapa tipe bawaan untuk T .

Dan tentu saja, jika fungsinya tidak mahal, Anda mungkin baik-baik saja melakukan perhitungan sebagai int64 / float64 dan mengekspos itu saja, menjaganya tetap sederhana dan tidak dibatasi pada T .

Kami benar-benar tidak dapat mengatakan kepada orang-orang "Anda dapat menulis fungsi generik pada tipe T apa pun tetapi fungsi generik tersebut mungkin tidak menggunakan konstanta yang tidak diketik." Go adalah bahasa yang sederhana. Bahasa dengan batasan aneh seperti itu tidak sederhana.

Setiap kali pendekatan yang diusulkan untuk obat generik menjadi sulit untuk dijelaskan dengan cara yang sederhana, kita harus membuang pendekatan itu. Lebih penting untuk menjaga bahasa tetap sederhana daripada menambahkan obat generik ke bahasa.

@JavierZunzunegui Salah satu properti menarik dari kode parameter (generik) adalah bahwa kompiler dapat menyesuaikannya berdasarkan jenis kode yang dipakai. Misalnya, seseorang mungkin ingin menggunakan tipe byte daripada int karena ini mengarah pada penghematan ruang yang signifikan (bayangkan sebuah fungsi yang mengalokasikan irisan besar dari tipe generik). Jadi membatasi kode hanya pada tipe "cukup besar" adalah jawaban yang tidak memuaskan, bahkan untuk bahasa "berpendapat" seperti Go.

Selain itu, ini bukan hanya tentang algoritme yang menggunakan konstanta tak bertipe "besar" yang mungkin tidak begitu umum: mengabaikan algoritme semacam itu dengan pertanyaan "berapa banyak dari itu" hanyalah lambaian tangan untuk menangkis masalah yang memang ada. Hanya untuk pertimbangan Anda: Tampaknya tidak masuk akal untuk sejumlah besar algoritme menggunakan konstanta bilangan bulat seperti -1, 0, 1. Perhatikan bahwa seseorang tidak dapat menggunakan -1 dalam hubungannya dengan bilangan bulat yang tidak diketik, hanya untuk memberi Anda contoh sederhana. Jelas kita tidak bisa mengabaikannya begitu saja. Kita harus bisa menentukan ini dalam kontrak.

@ianlancetaylor @griesemer terima kasih atas umpan baliknya - Saya dapat melihat ada konflik signifikan dalam perubahan yang saya usulkan dengan konstanta yang tidak diketik dan bilangan bulat negatif, saya akan meletakkannya di belakang saya.

Bisakah saya membawa perhatian Anda ke poin kedua di https://github.com/golang/go/issues/15292#issuecomment -546313279:

Perhatikan bahwa perubahan ini juga menguntungkan tipe yang tidak dideklarasikan sebelumnya. Di bawah proposal saat ini, diberikan tipe X struct{S string} (yang berasal dari perpustakaan eksternal, sehingga Anda tidak dapat menambahkan metode ke dalamnya), misalkan Anda memiliki []X dan ingin meneruskannya ke fungsi umum yang mengharapkan [ ]T, untuk T yang memenuhi kontrak Stringer. Itu akan membutuhkan tipe X2 X; func(x X2) String() string {return xS}, dan salinan dalam dari []X ke []X2. Di bawah usulan perubahan pada proposal ini, Anda menyimpan salinan dalam seluruhnya.

Relaksasi aturan konversi (jika memungkinkan secara teknis) akan tetap berguna.

@JavierZunzunegui Membahas konversi dari jenis []B([]A) jika B(a) (dengan a dari tipe A ) diizinkan tampaknya sebagian besar ortogonal untuk fitur umum. Saya pikir kita tidak perlu membawa ini ke sini.

@ianlancetaylor Saya tidak yakin seberapa relevan ini dengan Go, tetapi saya tidak berpikir konstanta benar-benar tidak diketik, mereka harus memiliki tipe karena kompiler harus memilih representasi mesin. Saya pikir istilah yang lebih baik adalah konstanta tipe tak tentu, karena konstanta dapat diwakili oleh beberapa tipe berbeda. Salah satu solusinya adalah dengan menggunakan tipe gabungan sehingga konstanta seperti 27 akan memiliki tipe seperti int16|int32|float16|float32 gabungan dari semua kemungkinan tipe . Kemudian T dalam tipe generik dapat menjadi tipe gabungan ini. Satu-satunya persyaratan adalah bahwa pada titik tertentu kita harus menyelesaikan penyatuan menjadi satu jenis. Kasus yang paling bermasalah akan menjadi sesuatu seperti print(27) karena tidak pernah ada satu jenis untuk diselesaikan, dalam kasus seperti itu semua jenis dalam serikat akan dilakukan, dan kita dapat memilih berdasarkan parameter pengoptimalan seperti ruang/kecepatan dll .

@keean Nama dan penanganan yang tepat dari apa yang disebut spesifikasi "konstanta tidak diketik" di luar topik dalam masalah ini. Mari kita bawa diskusi itu ke tempat lain. Terima kasih.

@ianlancetaylor Saya senang, namun Ini adalah salah satu alasan mengapa saya pikir Go tidak dapat memiliki implementasi generik yang bersih/sederhana, semua masalah ini saling berhubungan, dan pilihan awal yang dibuat untuk Go tidak diambil dengan mempertimbangkan pemrograman generik. Saya pikir bahasa lain, yang dirancang untuk membuat obat generik sederhana dengan desain diperlukan, untuk Go, obat generik akan selalu menjadi sesuatu yang ditambahkan ke bahasa nanti, dan pilihan terbaik untuk menjaga bahasa tetap bersih dan sederhana mungkin tidak memilikinya sama sekali.

Jika hari ini saya akan mendesain bahasa sederhana dengan waktu kompilasi yang cepat dan fleksibilitas yang sebanding, saya akan memilih metode yang berlebihan dan polimorfisme struktural (subtipe) melalui antarmuka golang dan tanpa obat generik. Sebenarnya itu akan memungkinkan kelebihan muatan melalui antarmuka anonim yang berbeda dengan bidang yang berbeda.

Memilih obat generik memiliki keuntungan dari penggunaan kembali kode yang bersih, tetapi ini menimbulkan lebih banyak noise yang menjadi rumit jika kendala ditambahkan yang terkadang mengarah ke kode yang sulit dipahami.
Lalu, jika kita memiliki obat generik, mengapa tidak menggunakan sistem batasan tingkat lanjut seperti klausa where, tipe yang lebih tinggi atau mungkin tipe peringkat yang lebih tinggi, dan juga tipe dependen?
Semua pertanyaan ini pada akhirnya akan muncul jika pergi mengadopsi obat generik, cepat atau lambat.

Menyatakannya dengan jelas, saya tidak menentang obat generik tetapi saya mempertimbangkan apakah itu cara untuk pergi untuk menghemat kesederhanaan go.

Jika pengenalan generik di go tidak dapat dihindari, maka masuk akal untuk merenungkan dampak pada waktu kompilasi ketika monomorfisasi fungsi generik.
Tidak akan menjadi default yang baik untuk kotak generik, yaitu menghasilkan satu salinan untuk semua jenis input bersama-sama, dan hanya mengkhususkan jika secara eksplisit diminta oleh pengguna dengan beberapa anotasi di definisi -atau situs panggilan?

Mengenai dampak pada kinerja runtime, ini akan mengurangi kinerja karena masalah tinju/unboxing, jika tidak, ada insinyur tingkat ahli c++ yang merekomendasikan obat generik tinju seperti yang dilakukan Java untuk mengurangi kesalahan cache.

@ianlancetaylor @griesemer Saya telah mempertimbangkan kembali masalah konstanta yang tidak diketik dan obat generik 'non-operator' (https://github.com/golang/go/issues/15292#issuecomment-547166519) dan telah menemukan cara yang lebih baik untuk menangani dengan itu.

Berikan tipe nummetik ( type MyInt32 int32 , type MyInt64 int64 , ...), ini memiliki banyak metode yang memenuhi kontrak yang sama ( Add(T) T , ...) akan mengambil risiko overflow func(MyInt64) FromI64(int64) MyInt64 tetapi tidak ~ func(MyInt32) FromI64(int64) MyInt32 ~. Ini memungkinkan penggunaan konstanta numerik (secara eksplisit ditetapkan ke nilai presisi terendah yang mereka butuhkan) dengan aman (1) karena tipe numerik presisi rendah tidak akan memenuhi kontrak yang diperlukan, tetapi semua yang lebih tinggi akan memenuhinya. Lihat playground , menggunakan antarmuka sebagai pengganti generik.

Keuntungan dari melonggarkan generik numerik di luar tipe bawaan (tidak spesifik untuk revisi terbaru ini, jadi saya seharusnya membagikannya minggu lalu) adalah memungkinkan instantiasi metode generik dengan tipe pengecekan overflow - lihat playground . Pemeriksaan overflow itu sendiri merupakan permintaan/proposal yang sangat populer (https://github.com/golang/go/issues/31500 dan masalah terkait).


(1) : Jaminan waktu kompilasi non-overflow untuk konstanta yang tidak diketik kuat dalam 'cabang' yang sama ( int[8/16/32/64] dan uint[8/16/32/64] ). Melintasi cabang, konstanta uint[X] hanya aman dipakai ke int[2X+] dan konstanta int[X] tidak dapat dibuat dengan aman oleh uint[X] sama sekali. Bahkan melonggarkan ini (memungkinkan int[X]<->uint[X] ) akan sederhana dan aman mengikuti beberapa standar minimal, dan secara kritis setiap kerumitan jatuh pada penulis kode generik, bukan pada pengguna generik (yang hanya peduli dengan kontrak , dan dapat mengharapkan tipe numerik apa pun yang memenuhinya adalah valid).

Metode generik - adalah kejatuhan Java!

@ianlancetaylor Saya senang, namun Ini adalah salah satu alasan mengapa saya pikir Go tidak dapat memiliki implementasi generik yang bersih/sederhana, semua masalah ini saling berhubungan, dan pilihan awal yang dibuat untuk Go tidak diambil dengan mempertimbangkan pemrograman generik. Saya pikir bahasa lain, yang dirancang untuk membuat obat generik sederhana dengan desain diperlukan, untuk Go, obat generik akan selalu menjadi sesuatu yang ditambahkan ke bahasa nanti, dan pilihan terbaik untuk menjaga bahasa tetap bersih dan sederhana mungkin tidak memilikinya sama sekali.

Saya setuju 100%. Saya sangat ingin melihat beberapa jenis obat generik diimplementasikan, saya pikir apa yang Anda masak saat ini akan menghancurkan kesederhanaan bahasa Go.

Gagasan saat ini untuk memperluas antarmuka terlihat seperti ini:

type I1(type P1) interface {
        m1(x P1)
}

type I2(type P1, P2) interface {
        m2(x P1) P2
        type int, float64
}

func f(type P1 I1(P1), P2 I2(P1, P2)) (x P1, y P2) P2

Maaf semuanya, tapi tolong jangan lakukan ini! Ini mempermalukan keindahan Go secara besar-besaran.

Setelah menulis hampir 100 ribu baris kode Go sekarang, saya baik-baik saja dengan tidak memiliki obat generik.

Namun, hal-hal kecil seperti mendukung

// Allow mulitple types in Slices and Maps declarations
func Reverse(s []<int,string>) {
    first := 0
    last := len(s) - 1
    for first < last {
        s[first], s[last] = s[last], s[first]
        first++
        last--
    }
}

//  Allow multiple types in variable declarations
func Index (s <string, []byte>, b byte) int {
    for i := 0; i < len(s); i++ {
        if s[i] == b {
            return i
        }
    }
    return -1
}

// Allow slices and maps declarations with interface values
func ToStrings (s []Stringer) []string {
    r := make([]string, len(s))
    for i, v := range s {
        r[i] = v.String()
    }
    return r
}

akan membantu.

Proposal sintaks untuk dapat memisahkan obat generik sepenuhnya dari kode Go biasa

package graph

// Example how you would define generics completely separat from Go 1 code
contract (Node, Edge)G {
    Node Edges() []Edge
    Edge Nodes() (from, to Node)
}

type (type Node, Edge G) ( Graph )
func (type Node, Edge G) ( New )
const _ = (Node, Edge) Graph

// Unmodified Go 1 code
type Graph struct { ... }
func New(nodes []Node) *Graph { ... }
func (g *Graph) ShortestPath(from, to Node) []Edge { ... }

@martinrode Namun, hal-hal kecil seperti mendukung
... izinkan beberapa jenis dalam deklarasi Slices dan Maps

Ini tidak menjawab kebutuhan untuk beberapa fungsi irisan generik fungsional misalnya head() , tail() , map(slice, func) , filter(slice, func)

Anda bisa menulisnya sendiri untuk setiap proyek yang Anda perlukan, tetapi pada saat itu berisiko menjadi basi karena pengulangan salin tempel dan mendorong kompleksitas kode Go untuk menghemat kesederhanaan bahasa.

(Pada tingkat pribadi, agak melelahkan juga mengetahui bahwa saya memiliki serangkaian fitur yang ingin saya terapkan dan tidak memiliki cara yang bersih untuk mengekspresikannya tanpa juga menjawab kendala bahasa)

Pertimbangkan hal berikut dalam go saat ini, non-generik:

Saya memiliki variabel x tipe externallib.Foo , diperoleh dari perpustakaan externallib Saya tidak mengontrol.
Saya ingin meneruskannya ke fungsi SomeFunc(fmt.Stringer) , tetapi externallib.Foo tidak memiliki metode String() string . Saya hanya bisa melakukan:

type MyFoo externallib.Foo
func (mf MyFoo) String() string {...}
// ...
SomeFunc(MyFoo(x))

Pertimbangkan hal yang sama dengan obat generik.

Saya memiliki variabel x bertipe []externallib.Foo . Saya ingin meneruskannya ke AnotherFunc(type T Stringer)(s []T) . Itu tidak dapat dilakukan tanpa penyalinan mendalam yang mahal dari irisan ke []MyFoo baru. Jika alih-alih sepotong itu adalah jenis yang lebih kompleks (misalnya, chan atau peta), atau metode memodifikasi penerima, itu menjadi lebih tidak efisien dan membosankan, jika memungkinkan.

Ini mungkin bukan masalah dalam pustaka standar, tetapi itu hanya karena ia tidak memiliki ketergantungan eksternal. Itu adalah kemewahan yang hampir tidak dimiliki proyek lain.

Saran saya adalah untuk melonggarkan konversi untuk mengizinkan []Foo([]Bar{}) untuk Foo yang didefinisikan sebagai type Foo Bar , atau sebaliknya, dan sama untuk peta, larik, saluran, dan penunjuk, secara rekursif. Perhatikan ini semua adalah salinan dangkal yang murah. Detail teknis selengkapnya di Proposal Konversi Tipe Santai .


Ini pertama kali diangkat sebagai fitur sekunder di https://github.com/golang/go/issues/15292#issuecomment -546313279.

@JavierZunzunegui Saya tidak berpikir itu benar-benar terkait dengan obat generik sama sekali. Ya, Anda dapat memberikan contoh menggunakan obat generik, tetapi Anda dapat memberikan contoh serupa tanpa menggunakan obat generik. Saya pikir masalah itu harus dibahas secara terpisah, bukan di sini. Lihat juga https://golang.org/doc/faq#convert_slice_with_same_underlying_type. Terima kasih.

Tanpa generik, konversi tersebut hampir tidak memiliki nilai sama sekali, karena secara umum []Foo tidak akan memenuhi antarmuka apa pun, atau setidaknya tidak ada antarmuka yang memanfaatkannya sebagai irisan. Pengecualiannya adalah antarmuka yang memiliki pola yang sangat spesifik untuk digunakan, seperti sort.Interface , di mana Anda tidak perlu mengonversi irisan.

Versi non-generik di atas ( func AnotherFunc(type T Stringer)(s []T) ) adalah

type SliceOfStringers interface {
  Len() int
  Get(int) fmt.Stringer
}
func AnotherFunc(s SliceOfStringers) {...}

Ini mungkin kurang praktis daripada pendekatan umum, tetapi dapat dibuat untuk menangani irisan apa pun dengan baik dan melakukannya tanpa menyalinnya, terlepas dari tipe dasarnya yang sebenarnya adalah fmt.Stringer . Seperti berdiri, obat generik tidak bisa, meskipun pada prinsipnya menjadi alat yang jauh lebih cocok untuk pekerjaan itu. Dan tentunya, jika kita menambahkan obat generik, justru membuat irisan, peta, dll. lebih umum di API, dan memanipulasinya dengan lebih sedikit boilerplate. Namun mereka memperkenalkan masalah baru, tanpa kesetaraan di dunia antarmuka saja, yang _mungkin_ bahkan tidak dapat dihindari tetapi secara artifisial dipaksakan oleh bahasa.

Konversi jenis yang Anda sebutkan cukup sering muncul dalam kode non-generik sehingga menjadi FAQ. Mari kita pindahkan diskusi ini ke tempat lain. Terima kasih.

Apa keadaan ini? Ada draf yang DIPERBARUI? Saya menunggu obat generik sejak
hampir 2 tahun yang lalu. Kapan kita akan memiliki obat generik?

El mar., 4 de feb. de 2020 a la 13:28, Ian Lance Taylor (
[email protected]) penjelasan:

Konversi jenis yang Anda sebutkan cukup sering muncul dalam kode non-generik
bahwa itu adalah FAQ. Mari kita pindahkan diskusi ini ke tempat lain. Terima kasih.


Anda menerima ini karena Anda disebutkan.
Balas email ini secara langsung, lihat di GitHub
https://github.com/golang/go/issues/15292?email_source=notifications&email_token=AJMFNBN3MFHDMENAFXIKBLDRBGXUTA5CNFSM4CA35RX2YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOKY
atau berhenti berlangganan
https://github.com/notifications/unsubscribe-auth/AJMFNBO5UTKNPL3MSA3NESLRBGXUTANCNFSM4CA35RXQ
.

--
Ini adalah tes untuk tanda tangan email yang akan digunakan di TripleMint

Kami sedang mengerjakannya. Beberapa hal memerlukan waktu.

Apakah pekerjaan dilakukan secara offline? Saya ingin melihatnya berkembang dari waktu ke waktu, dengan cara yang "publik umum" seperti saya tidak dapat berkomentar untuk menghindari kebisingan.

Meskipun sudah ditutup untuk menjaga diskusi obat generik di satu tempat, lihat #36177 di mana @Griesemer menautkan ke prototipe yang sedang dia kerjakan dan membuat beberapa komentar menarik tentang pemikirannya tentang masalah tersebut sejauh ini.

Saya pikir saya benar dengan mengatakan bahwa prototipe hanya berurusan dengan aspek pengecekan tipe dari proposal 'kontrak' saat ini tetapi pekerjaan itu pasti terdengar menjanjikan bagi saya.

@ianlancetaylor Setiap kali pendekatan yang diusulkan untuk obat generik menjadi sulit untuk dijelaskan dengan cara yang sederhana, kita harus membuang pendekatan itu. Lebih penting untuk menjaga bahasa tetap sederhana daripada menambahkan obat generik ke bahasa.

Itu adalah cita-cita yang bagus untuk diperjuangkan tetapi dalam kenyataannya pengembangan perangkat lunak pada waktunya secara inheren tidak _sederhana untuk dijelaskan_.

Ketika bahasa dibatasi untuk mengekspresikan ide-ide yang _tidak mudah diungkapkan_ seperti itu, para insinyur perangkat lunak akhirnya menemukan kembali fasilitas-fasilitas tersebut berulang kali, karena ide-ide terkutuk _sulit untuk diungkapkan_ ini terkadang penting bagi logika program.

Lihatlah Istio, Kubernetes, operator-sdk, dan sampai batas tertentu Terraform, dan bahkan perpustakaan protobuf. Mereka semua keluar dari sistem tipe Go dengan menggunakan refleksi, menerapkan sistem tipe baru di atas Go menggunakan antarmuka dan pembuatan kode, atau kombinasi dari semuanya.

@omeid

Lihat Istio, Kubernetes

Pernahkah terpikir oleh Anda bahwa alasan mereka melakukan hal-hal yang tidak masuk akal ini adalah karena desain inti mereka tidak masuk akal, dan akibatnya mereka harus menghasilkan game reflect untuk memenuhinya ?

Saya berpendapat bahwa desain yang lebih baik untuk program golang (baik dalam fase desain, dan dalam API) tidak _memerlukan_ obat generik.

Tolong jangan tambahkan mereka ke golang.

Pemrograman itu sulit. Kubelet adalah tempat yang gelap. Generik membagi orang lebih dari politik Amerika. Saya ingin percaya.

Ketika bahasa dibatasi untuk mengekspresikan ide-ide yang tidak sederhana, para insinyur perangkat lunak akhirnya menemukan kembali fasilitas-fasilitas itu lagi dan lagi, karena ide-ide yang sangat sulit untuk diungkapkan ini kadang-kadang penting bagi logika program.

Lihatlah Istio, Kubernetes, operator-sdk, dan sampai batas tertentu Terraform, dan bahkan perpustakaan protobuf. Mereka semua keluar dari sistem tipe Go dengan menggunakan refleksi, menerapkan sistem tipe baru di atas Go menggunakan antarmuka dan pembuatan kode, atau kombinasi dari semuanya.

Saya tidak menganggap itu sebagai argumen yang persuasif. Bahasa Go idealnya mudah dibaca, ditulis, dan dipahami, sambil tetap memungkinkan untuk melakukan operasi kompleks yang sewenang-wenang. Itu konsisten dengan apa yang Anda katakan: alat yang Anda sebutkan perlu melakukan sesuatu yang rumit, dan Go memberi mereka cara untuk melakukannya.

Bahasa Go idealnya mudah dibaca, ditulis, dan dipahami, sambil tetap memungkinkan untuk melakukan operasi kompleks yang sewenang-wenang.

Saya setuju dengan ini, tetapi karena itu adalah banyak tujuan, mereka kadang-kadang akan tegang satu sama lain. Kode yang secara alami "ingin" ditulis dalam gaya umum sering kali menjadi kurang mudah dibaca daripada jika harus menggunakan teknik seperti refleksi.

Kode yang secara alami "ingin" ditulis dalam gaya umum sering kali menjadi kurang mudah dibaca daripada jika harus menggunakan teknik seperti refleksi.

Itulah sebabnya proposal ini tetap terbuka dan mengapa kami memiliki draf desain untuk kemungkinan implementasi obat generik (https://blog.golang.org/why-generics).

Lihat ... bahkan perpustakaan protobuf. Mereka semua keluar dari sistem tipe Go dengan menggunakan refleksi, menerapkan sistem tipe baru di atas Go menggunakan antarmuka dan pembuatan kode, atau kombinasi dari semuanya.

Berbicara dari pengalaman dengan protobuf, ada beberapa kasus di mana obat generik dapat meningkatkan kegunaan dan/atau implementasi API, tetapi sebagian besar logika tidak akan mendapat manfaat dari obat generik. Generik menganggap bahwa informasi tipe konkret diketahui pada waktu kompilasi . Untuk protobuf, sebagian besar situasi melibatkan kasus di mana jenis informasi hanya diketahui saat runtime .

Secara umum, saya perhatikan bahwa orang sering menunjuk pada penggunaan refleksi dan mengklaim itu sebagai bukti perlunya obat generik. Hal ini tidak sesederhana ini. Perbedaan penting adalah apakah jenis informasi diketahui pada waktu kompilasi atau tidak. Dalam beberapa kasus pada dasarnya tidak.

@dsnet Terima kasih yang menarik, jangan pernah berpikir tentang protobuf yang tidak sesuai dengan standar. Selalu diasumsikan setiap alat yang menghasilkan kode boilerplate go seperti misalnya protoc, berdasarkan skema yang telah ditentukan, akan dapat menghasilkan kode generik tanpa refleksi menggunakan proposal generik saat ini. Maukah Anda memperbarui ini dalam spesifikasi dengan contoh atau dalam posting blog go baru di mana Anda menjelaskan masalah ini secara lebih rinci?

alat yang Anda sebutkan perlu melakukan sesuatu yang rumit, dan Go memberi mereka cara untuk melakukannya.

Menggunakan templat teks untuk menghasilkan kode Go bukanlah fasilitas menurut desainnya, menurut saya ini adalah bantuan pita ad-hoc, idealnya, setidaknya paket ast dan parser standar harus memungkinkan pembuatan kode Go yang sewenang-wenang.

Satu-satunya hal yang dapat Anda bantah bahwa Go memberikan seseorang untuk menangani logika kompleks mungkin adalah Refleksi, Tapi itu dengan cepat menunjukkan keterbatasannya, belum lagi kode kritis kinerja, bahkan ketika digunakan di perpustakaan standar, misalnya penanganan JSON Go primitif sebagus-bagusnya.

Sulit untuk membantah bahwa menggunakan templat teks atau refleksi untuk melakukan _sesuatu yang sudah rumit_ sesuai dengan ideal dari:

Setiap kali pendekatan yang diusulkan untuk ~generik~ sesuatu yang kompleks menjadi sulit untuk dijelaskan dengan cara yang sederhana, kita harus membuang pendekatan itu.

Saya pikir solusi yang disebutkan oleh proyek-proyek yang telah datang untuk memecahkan masalah mereka terlalu rumit, dan tidak mudah untuk dipahami. Jadi dalam hal itu, Go tidak memiliki fasilitas yang memungkinkan pengguna untuk mengekspresikan masalah yang kompleks dengan cara yang sederhana dan langsung mungkin.

Secara umum, saya perhatikan bahwa orang sering menunjuk pada penggunaan refleksi dan mengklaim itu sebagai bukti perlunya obat generik.

Mungkin ada kesalahpahaman umum seperti itu, tetapi perpustakaan protobuf, khususnya API baru bisa menjadi jauh lebih sederhana dengan _generics_, atau semacam _sum type_.

Salah satu penulis API protobuf baru itu baru saja mengatakan "sebagian besar logika tidak akan mendapat manfaat dari obat generik", jadi saya tidak yakin dari mana Anda mendapatkan bahwa "khususnya API baru dapat lebih banyak sederhana dengan obat generik". Ini berdasarkan apa? Bisakah Anda memberikan bukti bahwa itu akan jauh lebih sederhana?

Berbicara sebagai seseorang yang menggunakan API protobuf dalam beberapa bahasa yang menyertakan generik (Java, C++), saya tidak dapat mengatakan bahwa saya telah melihat perbedaan kegunaan yang signifikan dengan Go API dan API mereka. Jika pernyataan Anda benar, saya berharap ada beberapa perbedaan seperti itu.

@dsnet Juga mengatakan "ada beberapa kasus di mana obat generik dapat meningkatkan kegunaan dan/atau implementasi API".

Tetapi jika Anda ingin contoh bagaimana segala sesuatunya bisa lebih sederhana, mulailah dengan menghilangkan tipe Value karena sebagian besar merupakan tipe penjumlahan ad-hoc.

@omeid Masalah ini tentang obat generik, bukan tipe jumlah. Jadi saya tidak yakin bagaimana contoh itu relevan.

Secara khusus, pertanyaan saya adalah: bagaimana obat generik akan menghasilkan implementasi protobuf atau API yang "melompat dan jauh lebih sederhana" daripada API baru (atau lama, dalam hal ini)?

Ini sepertinya tidak sejalan dengan bacaan saya tentang apa yang dikatakan @dsnet di atas, atau dengan pengalaman saya dengan API protobuf Java dan C++.

Juga, komentar Anda tentang penanganan JSON primitif di Go juga menurut saya sama anehnya. Bisakah Anda menjelaskan bagaimana menurut Anda API encoding/json akan ditingkatkan oleh obat generik?

AFAIK, implementasi parsing JSON di Java menggunakan refleksi (bukan generik). Memang benar bahwa API tingkat atas di sebagian besar pustaka JSON kemungkinan akan menggunakan metode generik (mis. Gson ), tetapi metode yang menggunakan parameter generik yang tidak dibatasi T dan mengembalikan nilai tipe T menyediakan pemeriksaan tipe tambahan yang sangat sedikit jika dibandingkan dengan json.Unmarshal . Faktanya, saya pikir satu-satunya kesalahan bahwa satu-satunya skenario kesalahan tambahan yang tidak ditangkap oleh json.Unmarshal pada waktu kompilasi adalah jika Anda memberikan nilai non-pointer. (Juga, perhatikan peringatan dalam dokumentasi API Gson untuk menggunakan fungsi yang berbeda untuk tipe generik vs non-generik. Sekali lagi, ini berpendapat bahwa obat generik memperumit API mereka, daripada menyederhanakannya; dalam hal ini, ini untuk mendukung serialisasi/deserialisasi generik jenis).

(Dukungan JSON di C++ lebih buruk AFAICT; berbagai pendekatan yang saya tahu baik menggunakan sejumlah besar makro atau melibatkan penulisan fungsi parse/serialize secara manual. Sekali lagi, ini tidak)

Jika Anda mengharapkan obat generik untuk menambahkan banyak dukungan Go untuk JSON, saya khawatir Anda akan kecewa.


@gertcuykens Setiap implementasi protobuf dalam setiap bahasa yang saya ketahui menggunakan pembuatan kode, terlepas dari apakah mereka memiliki generik atau tidak. Ini termasuk Java, C++, Swift, Rust, JS (dan TS). Saya tidak berpikir memiliki obat generik secara otomatis menghapus semua penggunaan pembuatan kode (sebagai bukti keberadaan, saya telah menulis pembuat kode yang menghasilkan kode Java dan kode C++); tampaknya tidak logis untuk mengharapkan bahwa solusi apa pun untuk obat generik akan memenuhi standar itu.


Untuk memperjelas: Saya mendukung penambahan obat generik ke Go. Tapi saya pikir kita harus melihat dengan jelas tentang apa yang akan kita dapatkan darinya. Saya tidak yakin kami akan mendapatkan peningkatan yang signifikan baik pada protobuf atau JSON API.

Saya tidak berpikir protobuf adalah kasus yang sangat baik untuk obat generik. Anda tidak memerlukan obat generik dalam bahasa target karena Anda cukup membuat kode khusus secara langsung. Ini akan berlaku untuk sistem serupa lainnya seperti Swagger/OpenAPI juga.

Di mana obat generik tampaknya berguna bagi saya, dan dapat menawarkan penyederhanaan dan keamanan jenis dalam penulisan kompiler protobuf itu sendiri.

Yang Anda perlukan adalah bahasa yang mampu merepresentasikan tipe aman dari pohon sintaksis abstraknya sendiri. Dari pengalaman saya sendiri, ini membutuhkan setidaknya generik dan Tipe Data Abstrak Umum. Anda kemudian dapat menulis kompiler protobuf tipe-safe untuk bahasa dalam bahasa itu sendiri.

Di mana obat generik tampaknya berguna bagi saya, dan dapat menawarkan penyederhanaan dan keamanan jenis dalam penulisan kompiler protobuf itu sendiri.

Saya tidak benar-benar melihat bagaimana. Paket go/ast sudah menyediakan representasi dari AST Go. Kompiler Go protobuf tidak menggunakannya karena bekerja dengan AST jauh lebih rumit daripada hanya memancarkan string, bahkan jika itu lebih aman untuk tipe.

Mungkin Anda memiliki contoh dari kompiler protobuf untuk beberapa bahasa lain?

@neild Saya mulai dengan mengatakan bahwa saya tidak berpikir protobuf adalah contoh yang sangat baik. Ada keuntungan yang dapat diperoleh dengan menggunakan obat generik, tetapi mereka sangat bergantung pada seberapa penting keamanan tipe bagi Anda, dan ini akan diimbangi dengan seberapa mengganggu implementasi obat generik. Implementasi yang ideal akan keluar dari jalan Anda, kecuali jika Anda membuat kesalahan, dan dalam hal ini keuntungannya akan lebih besar daripada biaya untuk lebih banyak kasus penggunaan.

Melihat paket go/ast, itu tidak memiliki representasi AST yang diketik karena itu membutuhkan obat generik dan GADT. Misalnya, simpul 'tambah' harus generik dalam jenis istilah yang ditambahkan. Dengan AST yang tidak aman untuk tipe, semua logika pemeriksaan tipe harus dikodekan dengan tangan yang akan membuatnya tidak praktis.

Dengan sintaks template yang baik dan ketik ekspresi aman, Anda dapat membuatnya semudah memancarkan string, tetapi juga mengetik aman. Misalnya lihat (ini lebih lanjut tentang sisi parsing): https://stackoverflow.com/questions/11104536/how-to-parse-strings-to-syntax-tree-using-gadts

Misalnya pertimbangkan JSX sebagai sintaks literal untuk HTML Dom di JavaScript Vs TSX sebagai sintaks literal untuk Dom di TypeScript.

Kita dapat menulis ekspresi generik yang diketik yang mengkhususkan diri pada kode akhir. Mudah ditulis sebagai string, tetapi ketik dicentang (dalam bentuk generiknya).

Salah satu masalah utama dengan pembuat kode adalah bahwa pengecekan tipe hanya terjadi pada kode yang dipancarkan, yang membuat penulisan template yang benar menjadi sulit. Dengan obat generik, Anda dapat menulis templat sebagai ekspresi yang diperiksa tipe sebenarnya, jadi pemeriksaan dilakukan langsung pada templat, bukan kode yang dipancarkan, yang membuatnya lebih mudah untuk memperbaikinya, dan memeliharanya.

Parameter tipe variadik tidak ada dalam desain saat ini, yang terlihat seperti kehilangan besar fungsi generik. Desain tambahan (mungkin) mengikuti desain kontrak saat ini:

contract Comparables(Ts...) {
    if  len(Ts) > 0 {
        Comparables(Ts[1:]...)
    } else {
        Comparable(Ts[0])
    }
}

contract Comparable(T) {
    T int, int8, int16, int32, int64,
        uint, uint8, uint16, uint32, uint64, uintptr,
        float32, float64,
        string
}

type Keys(type Ts ...Comparables) struct {
    fs ...Ts
}

type Metric(type Ts ...Comparables) struct {
    mu sync.Mutex
    m  map[Keys(Ts...)]int
}

func (m *Metric(Ts...)) Add(vs ...Ts) {
    m.mu.Lock()
    defer m.mu.Unlock()
    if m.m == nil {
        m.m = make(map[Keys(Ts...))]int)
    }
    m[Keys(Ts...){vs...}]++
}


// To use the metric

m := Metric(int, float64, string){m: make(map[Keys(int, float64, string)]int}
m.Add(1, 2.0, "variadic")

Contoh terinspirasi dari sini .

Tidak jelas bagi saya bagaimana itu menambahkan keamanan apa pun di atas hanya menggunakan interface{} . Apakah ada masalah nyata dengan orang-orang yang meneruskan yang tidak dapat dibandingkan ke dalam metrik?

Tidak jelas bagi saya bagaimana itu menambahkan keamanan apa pun di atas hanya menggunakan interface{} . Apakah ada masalah nyata dengan orang-orang yang meneruskan yang tidak dapat dibandingkan ke dalam metrik?

Comparables dalam contoh ini membutuhkan Keys harus terdiri dari serangkaian jenis yang sebanding. Ide utamanya adalah untuk menunjukkan desain parameter tipe variadik, bukan arti dari tipe itu sendiri.

Saya tidak ingin terlalu terpaku pada contoh, tetapi saya memilihnya karena saya pikir banyak contoh "ekstensi tipe" hanya berakhir dengan mendorong pembukuan tanpa menambahkan keamanan praktis. Dalam hal ini, jika Anda melihat tipe yang buruk pada saat run time atau berpotensi dengan pergi ke dokter hewan, Anda bisa mengeluh.

Juga, saya sedikit khawatir bahwa mengizinkan tipe tipe terbuka seperti ini akan menyebabkan masalah referensi paradoks, seperti yang terjadi pada logika orde kedua. Bisakah Anda mendefinisikan C sebagai kontrak dari semua jenis yang tidak ada di C?

Juga, saya sedikit khawatir bahwa mengizinkan tipe tipe terbuka seperti ini akan menyebabkan masalah referensi paradoks, seperti yang terjadi pada logika orde kedua. Bisakah Anda mendefinisikan C sebagai kontrak dari semua jenis yang tidak ada di C?

Maaf tapi saya tidak mengerti bagaimana contoh ini mengizinkan tipe terbuka dan berhubungan dengan paradoks Russell, Comparables didefinisikan oleh daftar Comparable .

Saya tidak suka ide menulis kode Go di dalam kontrak. Jika saya dapat menulis pernyataan if , dapatkah saya menulis pernyataan for ? Bisakah saya memanggil fungsi? Bisakah saya mendeklarasikan variabel? Kenapa tidak?

Tampaknya juga tidak perlu. func F(a ...int) berarti a adalah []int . Dengan analogi, func F(type Ts ...comparable) berarti bahwa setiap jenis dalam daftar adalah comparable .

Di baris-baris ini

type Keys(type Ts ...Comparables) struct {
    fs ...Ts
}

Anda tampaknya mendefinisikan struct dengan beberapa bidang semuanya bernama fs . Saya tidak yakin bagaimana cara kerjanya. Apakah ada cara untuk menggunakan referensi ke bidang dalam struct ini selain menggunakan refleksi?

Jadi pertanyaannya adalah: apa yang dapat dilakukan dengan parameter tipe variadik? Apa yang ingin dilakukan seseorang?

Di sini saya pikir Anda menggunakan parameter tipe variadic untuk mendefinisikan tipe Tuple dengan jumlah bidang yang berubah-ubah.

Apa lagi yang ingin dilakukan seseorang?

Saya tidak suka ide menulis kode Go di dalam kontrak. Jika saya dapat menulis pernyataan if , dapatkah saya menulis pernyataan for ? Bisakah saya memanggil fungsi? Bisakah saya mendeklarasikan variabel? Kenapa tidak?

Tampaknya juga tidak perlu. func F(a ...int) berarti a adalah []int . Dengan analogi, func F(type Ts ...comparable) berarti bahwa setiap jenis dalam daftar adalah comparable .

Setelah meninjau contoh sehari kemudian, saya pikir Anda benar sekali. Comparables adalah ide yang bodoh. Contoh hanya ingin menyampaikan pesan penggunaan len(args) untuk menentukan jumlah parameter. Ternyata untuk fungsi, func F(type Ts ...Comparable) sudah cukup baik.

Contoh yang dipangkas:

contract Comparable(T) {
    T int, int8, int16, int32, int64,
        uint, uint8, uint16, uint32, uint64, uintptr,
        float32, float64,
        string
}

type Keys(type Ts ...Comparable) struct {
    fs ...Ts
}

type Metric(type Ts ...Comparable) struct {
    mu sync.Mutex
    m  map[Keys(Ts...)]int
}

func (m *Metric(Ts...)) Add(vs ...Ts) {
    m.mu.Lock()
    defer m.mu.Unlock()
    if m.m == nil {
        m.m = make(map[Keys(Ts...))]int)
    }
    m[Keys(Ts...){vs...}]++
}


// To use the metric

m := Metric(int, float64, string){m: make(map[Keys(int, float64, string)]int}
m.Add(1, 2.0, "variadic")

Anda tampaknya mendefinisikan struct dengan beberapa bidang semuanya bernama fs . Saya tidak yakin bagaimana cara kerjanya. Apakah ada cara untuk menggunakan referensi ke bidang dalam struct ini selain menggunakan refleksi?

Jadi pertanyaannya adalah: apa yang dapat dilakukan dengan parameter tipe variadik? Apa yang ingin dilakukan seseorang?

Di sini saya pikir Anda menggunakan parameter tipe variadic untuk mendefinisikan tipe Tuple dengan jumlah bidang yang berubah-ubah.

Apa lagi yang ingin dilakukan seseorang?

Parameter tipe variadik ditujukan untuk tupel menurut definisinya jika kita menggunakan ... untuk itu, tidak berarti bahwa tupel adalah satu-satunya kasus penggunaan, tetapi seseorang dapat menggunakannya di semua struct dan fungsi apa pun.

Karena hanya ada dua tempat yang muncul dengan parameter tipe variadic: struct atau function, jadi kita dengan mudah memiliki apa yang jelas sebelumnya untuk fungsi:

func F(type Ts ...Comparable) (args ...Ts) {
    if len(args) > 1 {
        F(args[1:])
        return
    }
    // ... do stuff with args[0]
}

Misalnya, fungsi variadic Min tidak dimungkinkan dalam desain saat ini, tetapi dimungkinkan dengan parameter tipe variadic:

func Min(type T ...Comparable)(p1 T, pn ...T) T {
    switch l := len(pn); {
    case l > 1:
        return Min(pn[0], pn[1:]...)
    case l == 1:
        if p1 >= pn[0] { return pn[0] }
        return p1
    case l < 1:
        return p1
    }
}

Untuk mendefinisikan Tuple dengan parameter tipe variadik:

type Tuple(type Ts ...Comparable) struct {
    fs ...Ts
}

Ketika tiga parameter tipe dipakai oleh 'Ts`, itu dapat diterjemahkan ke

type Tuple(type T1, T2, T3 Comparable) struct {
    fs_1 T1
    fs_2 T2
    fs_3 T3
}

sebagai representasi perantara. Untuk menggunakan fs , ada beberapa cara:

  1. membongkar parameter
k := Tuple(int, float64, string){1, 2.0, "variadic"}
fs1, fs2, fs3 := k.fs // translated to fs1, fs2, fs3 := k.fs_1, k.fs_2, k.fs_3
println(fs1) // 1
println(fs2) // 2.0
println(fs3) // variadic
  1. gunakan for loop
for idx, f := range k.fs {
    println(idx, ": ", f)
}
// Output:
// 0: 1
// 1: 2.0
// 2: variadic
  1. gunakan indeks (tidak yakin apakah orang melihat ini adalah ambiguitas untuk array/slice atau peta)
k.fs[0] = ... // translated to k.fs_1 = ...
f2 := k.fs[1] // translated to f2 := k.fs_2
  1. gunakan paket reflect , pada dasarnya berfungsi seperti array
t := Tuple(int, float64, string){1, 2.0, "variadic"}

fs := reflect.ValueOf(t).Elem().FieldByName("fs")
val := reflect.ValueOf(fs)
if val.Kind() == reflect.VariadicTypes {
    for i := 0; i < val.Len(); i++ {
        e := val.Index(i)
        switch e.Kind() {
        case reflect.Int:
            fmt.Printf("%v, ", e.Int())
        case reflect.Float64:
            fmt.Printf("%v, ", e.Float())
        case reflect.String:
            fmt.Printf("%v, ", e.String())
        }
    }
}

Tidak ada yang benar-benar baru dibandingkan dengan penggunaan array.

Misalnya, fungsi Min variadik tidak dimungkinkan dalam desain saat ini, tetapi dimungkinkan dengan parameter tipe variadik:

func Min(type T ...Comparable)(p1 T, pn ...T) T {
    switch l := len(pn); {
    case l > 1:
        return Min(pn[0], pn[1:]...)
    case l == 1:
        if p1 >= pn[0] { return pn[0] }
        return p1
    case l < 1:
        return p1
    }
}

Ini tidak masuk akal bagi saya. Parameter tipe variadik hanya masuk akal jika tipenya bisa tipe yang berbeda. Tetapi memanggil Min pada daftar jenis yang berbeda tidak masuk akal. Go tidak mendukung penggunaan >= pada nilai dari tipe yang berbeda. Bahkan jika kami mengizinkannya, kami mungkin akan diminta Min(int, string)(1, "a") . Itu tidak memiliki jawaban apa pun.

Meskipun benar bahwa desain saat ini tidak mengizinkan Min dari sejumlah variadik dari jenis yang berbeda, itu mendukung pemanggilan Min pada sejumlah nilai variadik dari jenis yang sama. Yang menurut saya ini adalah satu-satunya cara yang masuk akal untuk menggunakan Min bagaimanapun juga.

func Min(type T comparable)(s ...T) T {
    if len(s) == 0 {
        panic("Min of no elements")
    }
    r := s[0]
    for _, v := range s[1:] {
        if v < r {
            r = v
        }
    }
    return r
}

Untuk beberapa contoh lain di https://github.com/golang/go/issues/15292#issuecomment -599040081, penting untuk dicatat bahwa di Go irisan dan larik memiliki elemen yang semuanya bertipe sama. Saat menggunakan parameter tipe variadik, elemennya adalah tipe yang berbeda. Jadi sebenarnya tidak sama dengan slice atau array.

Meskipun benar bahwa desain saat ini tidak mengizinkan Min dari sejumlah variadik dari jenis yang berbeda, itu mendukung pemanggilan Min pada sejumlah nilai variadik dari jenis yang sama. Yang saya pikir itu adalah satu-satunya cara yang masuk akal untuk menggunakan Min bagaimanapun juga.

func Min(type T comparable)(s ...T) T {
    if len(s) == 0 {
        panic("Min of no elements")
    }
    r := s[0]
    for _, v := range s[1:] {
        if v < r {
            r = v
        }
    }
    return r
}

Benar. Min adalah contoh yang buruk. Itu ditambahkan terlambat dan tidak memiliki pemikiran yang jelas, seperti yang Anda lihat dari riwayat edit komentar. Contoh nyata adalah Metric yang Anda abaikan.

penting untuk dicatat bahwa di Go, irisan dan larik memiliki elemen dengan tipe yang sama. Saat menggunakan parameter tipe variadik, elemennya adalah tipe yang berbeda. Jadi sebenarnya tidak sama dengan slice atau array.

Melihat? Anda adalah orang-orang yang melihat ini sebagai ambiguitas untuk array/slice atau peta. Seperti yang saya katakan di https://github.com/golang/go/issues/15292#issuecomment -599040081 , sintaksnya sangat mirip dengan array/slice dan map, tetapi mengakses elemen dengan tipe yang berbeda. Apakah itu benar-benar penting? Atau dapatkah seseorang membuktikan bahwa ini adalah ambiguitas? Apa yang mungkin di Go 1 adalah:

m := map[interface{}]int{1: 2, "2": 3, 3.0: 4}
for i, e := range m {
    println(i, e)
}

Apakah i dianggap sebagai jenis yang sama? Rupanya, kami mengatakan i adalah interface{} , tipe yang sama. Tetapi apakah antarmuka benar-benar mengekspresikan tipenya? Pemrogram harus memeriksa secara manual jenis apa yang mungkin. Saat menggunakan for , [] dan membongkar, apakah mereka benar-benar penting bagi pengguna bahwa mereka tidak mengakses jenis yang sama? Apa argumen yang menentang ini? Sama untuk fs :

for idx, f := range k.fs {
    switch f.(type) { // compare to interface{}, here is zero overhead.
    case int:
        // ...
    case float64:
        // ...
    case string:
        // ...
    }
}

Jika Anda harus menggunakan sakelar tipe untuk mengakses elemen tipe generik variadik, saya tidak melihat keuntungannya. Saya dapat melihat bagaimana dengan beberapa pilihan teknik kompilasi mungkin sedikit lebih efisien saat dijalankan daripada menggunakan interface{} . Tapi saya pikir perbedaannya akan cukup kecil, dan saya tidak melihat mengapa itu akan lebih aman untuk tipe. Tidak segera jelas bahwa ada baiknya membuat bahasa lebih kompleks.

Saya tidak bermaksud mengabaikan contoh Metric , saya hanya belum melihat bagaimana menggunakan tipe generik variadic untuk membuatnya lebih mudah untuk ditulis. Jika saya perlu menggunakan sakelar tipe di badan Metric , maka saya pikir saya lebih suka menulis Metric2 dan Metric3 .

Apa definisi dari "membuat bahasa lebih kompleks"? Kita semua setuju bahwa obat generik adalah hal yang kompleks, dan itu tidak akan pernah membuat bahasa lebih sederhana daripada Go 1. Anda telah berusaha keras untuk merancang dan mengimplementasikannya, tetapi cukup tidak jelas bagi pengguna Go: apa definisi dari "terasa seperti menulis... Pergi"? Apakah ada metrik terukur untuk mengukurnya? Bagaimana proposal bahasa dapat berargumen bahwa itu tidak membuat bahasa menjadi lebih kompleks? Dalam template proposal bahasa Go 2, tujuannya cukup jelas dalam kesan pertamanya:

  1. mengatasi masalah penting bagi banyak orang,
  2. memiliki dampak minimal pada orang lain, dan
  3. datang dengan solusi yang jelas dan dipahami dengan baik.

Tapi, pertanyaannya bisa: Berapa banyak yang "banyak"? Apa singkatan dari "penting"? Bagaimana mengukur dampak pada populasi yang tidak diketahui? Kapan suatu masalah dipahami dengan baik? Go mendominasi cloud, tetapi akankah mendominasi area lain seperti komputasi numerik ilmiah (misalnya, pembelajaran mesin), rendering grafis (misalnya, pasar 3D yang besar) menjadi salah satu target Go? Apakah masalahnya lebih cocok di "Saya lebih suka melakukan A daripada B di Go & Tidak ada use case karena kami dapat melakukannya dengan cara lain" atau "B tidak ditawarkan, oleh karena itu kami tidak menggunakan Go & Use case belum ada karena bahasa tidak mudah mengungkapkannya"? ... Saya menemukan pertanyaan-pertanyaan itu menyakitkan dan tidak ada habisnya, dan kadang-kadang bahkan tidak layak untuk dijawab.

Kembali ke contoh Metric , itu tidak menunjukkan kebutuhan untuk mengakses individu. Membongkar set parameter tampaknya bukan kebutuhan nyata di sini, meskipun solusi yang "bertepatan" dengan bahasa yang ada menggunakan pengindeksan [ ] dan pengurangan tipe dapat memecahkan masalah tipe-safe:

f2 := k.fs[1] // f2 is a float64

@changkun Jika ada metrik yang jelas dan objektif untuk memutuskan fitur bahasa apa yang baik dan buruk, kami tidak memerlukan perancang bahasa - kami hanya dapat menulis program untuk merancang bahasa yang optimal untuk kami. Tapi tidak ada - itu selalu bermuara pada preferensi pribadi dari beberapa kelompok orang. Yang juga, BTW, mengapa tidak masuk akal untuk memperdebatkan apakah suatu bahasa "baik" atau tidak - satu-satunya pertanyaan adalah apakah Anda, secara pribadi, menyukainya. Dalam kasus Go, orang-orang yang menentukan preferensi adalah orang-orang di tim Go dan hal-hal yang Anda kutip bukanlah metrik, mereka memandu pertanyaan untuk membantu Anda meyakinkan mereka.

Secara pribadi, FWIW, saya merasa parameter tipe variadic gagal pada dua dari ketiganya. Saya tidak berpikir mereka membahas masalah penting bagi banyak orang - contoh metrik mungkin mendapat manfaat dari mereka, tetapi IMO hanya sedikit dan ini adalah kasus penggunaan yang sangat khusus. Dan saya tidak berpikir mereka datang dengan solusi yang jelas dan dipahami dengan baik. Saya tidak mengetahui bahasa apa pun yang mendukung sesuatu seperti ini. Tapi saya mungkin salah. Pasti akan sangat membantu, jika seseorang memiliki contoh bahasa lain yang mendukung ini - ini dapat memberikan info tentang bagaimana biasanya diterapkan dan yang lebih penting, bagaimana itu digunakan. Mungkin itu digunakan lebih luas daripada yang bisa saya bayangkan.

@Merovius Haskell memiliki fungsi polivariadik seperti yang kami tunjukkan dalam makalah HList: http://okmij.org/ftp/Haskell/polyvariadic.html#polyvar -fn
Jelas rumit untuk melakukan ini di Haskell, tetapi bukan tidak mungkin.

Contoh motivasi adalah jenis akses database yang aman di mana hal-hal seperti jenis aman bergabung dan proyeksi dapat dilakukan, dan skema database dideklarasikan dalam bahasa.

Misalnya tabel database sangat mirip dengan record, di mana terdapat nama dan tipe kolom. Operasi join relasional mengambil dua record arbitrer dan menghasilkan record dengan tipe dari keduanya. Anda tentu saja dapat melakukan ini dengan tangan, tetapi rentan terhadap kesalahan, sangat membosankan, mengaburkan arti kode dengan semua jenis catatan yang dideklarasikan dengan tangan, dan tentu saja fitur besar dari database SQL adalah mendukung ad-hoc kueri, jadi Anda tidak bisa membuat semua jenis rekaman yang mungkin, karena Anda tidak perlu tahu kueri apa yang Anda inginkan sampai Anda melakukannya.

Jadi operator gabungan relasional tipe-aman pada catatan dan tupel akan menjadi kasus penggunaan yang baik. Kami hanya memikirkan jenis fungsi di sini - terserah programmer apa fungsi sebenarnya, apakah itu gabungan memori dari dua array tupel, atau apakah itu menghasilkan SQL untuk dijalankan pada DB eksternal dan menyusun hasilnya kembali dengan cara yang aman.

Hal semacam ini mendapat penyematan yang jauh lebih rapi di C# dengan LINQ. Kebanyakan orang tampaknya menganggap LINQ sebagai menambahkan fungsi lambda dan monad ke C#, tetapi itu tidak akan berfungsi untuk kasus penggunaan utamanya tanpa polivariadik, karena Anda tidak dapat menentukan jenis operator gabung yang aman tanpa fungsi serupa.

Saya pikir operator relasional itu penting. Setelah operator dasar pada tipe Boolean, biner, int, float dan string, set mungkin datang berikutnya, dan kemudian relasi.

BTW, C++ juga menawarkannya meskipun kami tidak ingin berdebat bahwa kami menginginkan fitur ini di Go karena XXX memilikinya :)

Saya pikir akan sangat aneh jika k.fs[0] dan k.fs[1] memiliki tipe yang berbeda. Itu bukan cara kerja nilai yang dapat diindeks lainnya di Go.

Contoh metrik didasarkan pada https://medium.com/@sameer_74231/go -experience-report-for-generics-google-metrics-api-b019d597aaa4. Saya pikir kode itu memerlukan refleksi untuk mengambil nilainya. Saya pikir jika kita akan menambahkan generik variadic ke Go, kita harus mendapatkan sesuatu yang lebih baik daripada refleksi untuk mengambil nilainya. Kalau tidak, sepertinya tidak banyak membantu.

Saya pikir akan sangat aneh jika k.fs[0] dan k.fs[1] memiliki tipe yang berbeda. Itu bukan cara kerja nilai yang dapat diindeks lainnya di Go.

Contoh metrik didasarkan pada https://medium.com/@sameer_74231/go -experience-report-for-generics-google-metrics-api-b019d597aaa4. Saya pikir kode itu memerlukan refleksi untuk mengambil nilainya. Saya pikir jika kita akan menambahkan generik variadic ke Go, kita harus mendapatkan sesuatu yang lebih baik daripada refleksi untuk mengambil nilainya. Kalau tidak, sepertinya tidak banyak membantu.

Sehat. Anda meminta sesuatu tidak ada. Jika Anda tidak menyukai [``] , ada dua opsi tersisa: ( ) atau {``} , dan saya melihat Anda dapat berargumen bahwa tanda kurung terlihat seperti pemanggilan fungsi dan kurung kurawal terlihat seperti inisialisasi variabel. Tidak ada yang menyukai args.0 args.1 karena ini tidak terasa seperti Go. Sintaksnya sepele.

Sebenarnya, saya menghabiskan waktu akhir pekan membaca buku "desain dan evolusi C++", ada banyak wawasan menarik tentang keputusan dan pelajaran meskipun ditulis pada tahun 1994:

_"[...] Dalam retrospeksi, saya meremehkan pentingnya kendala dalam keterbacaan dan deteksi kesalahan dini."_ ==> Desain kontrak yang bagus

"_Sintaks fungsi sekilas juga terlihat lebih bagus tanpa kata kunci tambahan:_

T& index<class T>(vector<T>& v, int i) { /*...*/ }
int i = index(v1, 10);

_Tampaknya ada masalah yang mengganggu dengan sintaks yang lebih sederhana ini. Ini terlalu pintar. Relatif sulit untuk menemukan deklarasi template dalam sebuah program karena [...] Tanda kurung <...> dipilih daripada tanda kurung karena pengguna menganggapnya lebih mudah dibaca. [...] Seperti yang terjadi, Tom Pennello membuktikan bahwa tanda kurung akan lebih mudah untuk diuraikan, tetapi itu tidak mengubah pengamatan utama bahwa pembaca (manusia) lebih suka <...> _
" ==> bukankah itu mirip dengan func F(type T C)(v T) T ?

_"Namun, saya merasa bahwa saya terlalu berhati-hati dan konservatif dalam hal menentukan fitur template. Saya dapat menyertakan fitur seperti [...]. Fitur ini tidak akan menambah beban pelaksana, dan pengguna akan terbantu."_

Kenapa rasanya begitu akrab?

Pengindeksan parameter tipe variadik (atau tupel) perlu terpisah untuk pengindeksan runtime dan pengindeksan waktu kompilasi. Saya kira Anda mungkin hanya berpendapat bahwa kurangnya dukungan untuk pengindeksan runtime dapat membingungkan pengguna karena tidak konsisten dengan pengindeksan waktu kompilasi. Bahkan untuk pengindeksan waktu kompilasi, parameter "templat" non-tipe juga tidak ada dalam desain saat ini.

Dengan semua bukti, proposal (kecuali laporan pengalaman) mencoba menghindari membahas fitur ini, dan saya mulai percaya ini bukan tentang menambahkan generik variadik ke Go tetapi hanya dihapus oleh desain.

Saya setuju bahwa Desain dan Evolusi C++ adalah buku yang bagus, tetapi C++ dan Go memiliki tujuan yang berbeda. Kutipan terakhir ada yang bagus; Stroustrup bahkan tidak menyebutkan biaya kerumitan bahasa bagi pengguna bahasa tersebut. Di Go kami selalu berusaha mempertimbangkan biaya itu. Go dimaksudkan untuk menjadi bahasa yang sederhana. Jika kami menambahkan setiap fitur yang akan membantu pengguna, itu tidak akan mudah. Karena C++ tidak sederhana.

Dengan semua bukti, proposal (kecuali laporan pengalaman) mencoba menghindari membahas fitur ini, dan saya mulai percaya ini bukan tentang menambahkan generik variadik ke Go tetapi hanya dihapus oleh desain.

Maaf, saya tidak tahu apa yang Anda maksud di sini.

Secara pribadi saya selalu mempertimbangkan kemungkinan tipe generik variadik, tetapi saya tidak pernah meluangkan waktu untuk mencari tahu cara kerjanya. Cara kerjanya di C++ sangat halus. Saya ingin melihat apakah pertama-tama kita bisa membuat obat generik non-variadik berfungsi. Pasti ada waktu untuk menambahkan obat generik variadik, jika memungkinkan, nanti.

Ketika saya mengkritik pemikiran sebelumnya, saya tidak mengatakan bahwa tipe variadik tidak dapat dilakukan. Saya menunjukkan masalah yang menurut saya perlu diselesaikan. Jika mereka tidak dapat diselesaikan, maka saya tidak yakin bahwa tipe variadik sepadan.

Stroustrup bahkan tidak menyebutkan biaya kerumitan bahasa bagi pengguna bahasa tersebut. Di Go kami selalu berusaha mempertimbangkan biaya itu. Go dimaksudkan untuk menjadi bahasa yang sederhana. Jika kami menambahkan setiap fitur yang akan membantu pengguna, itu tidak akan mudah. Karena C++ tidak sederhana.

Tidak benar IMO. Satu harus dicatat bahwa C++ adalah praktisi pertama yang meneruskan obat generik (Yah ML adalah bahasa pertama). Dari apa yang saya baca dari buku, saya mendapatkan pesan bahwa C++ dimaksudkan untuk menjadi bahasa yang sederhana (tidak menawarkan obat generik pada awalnya, Eksperimen-Sederhanakan-Kapal loop untuk desain bahasa, riwayat yang sama). C++ juga memiliki fase pembekuan fitur selama beberapa tahun yang kami miliki di Go "The Compatability Promise". Tapi itu menjadi sedikit di luar kendali dari waktu ke waktu karena banyak alasan yang masuk akal, yang tidak jelas untuk Go jika menangkap jalur lama C++ setelah rilis obat generik.

Pasti ada waktu untuk menambahkan obat generik variadik, jika memungkinkan, nanti.

Perasaan yang sama padaku. Obat generik variadik juga tidak ada dalam versi templat standar pertama.

Saya menunjukkan masalah yang menurut saya perlu diselesaikan. Jika mereka tidak dapat diselesaikan, maka saya tidak yakin bahwa tipe variadik sepadan.

Saya memahami kekhawatiran Anda. Tetapi masalahnya pada dasarnya terpecahkan tetapi hanya perlu diterjemahkan dengan benar ke Go (dan saya kira tidak ada yang suka kata "terjemahkan"). Apa yang saya baca dari proposal generik historis Anda, mereka pada dasarnya mengikuti apa yang gagal dalam proposal awal C++ dan berkompromi dengan apa yang disesali Stroustrup. Saya tertarik dengan argumen kontra Anda tentang ini.

Kita harus tidak setuju tentang tujuan C++. Mungkin tujuan awalnya lebih mirip, tetapi melihat C++ hari ini, saya pikir jelas bahwa tujuan mereka sangat berbeda dari tujuan untuk Go, dan saya pikir itu telah terjadi setidaknya selama 25 tahun.

Dalam menulis berbagai proposal untuk menambahkan obat generik ke Go, saya tentu saja melihat cara kerja template C++, serta melihat banyak bahasa lain (bagaimanapun, C++ tidak menemukan obat generik). Saya tidak melihat apa yang disesali Stroustrup, jadi jika kita datang ke tempat yang sama, maka, bagus. Pemikiran saya adalah bahwa obat generik di Go lebih mirip obat generik di Ada atau D daripada seperti C++. Bahkan hari ini, C++ tidak memiliki kontrak, yang mereka sebut konsep tetapi belum ditambahkan ke bahasa. Juga, C++ sengaja mengizinkan pemrograman kompleks pada waktu kompilasi, dan sebenarnya template C++ itu sendiri adalah bahasa lengkap Turing (walaupun saya tidak tahu apakah itu disengaja). Saya selalu menganggap itu sebagai sesuatu yang harus dihindari untuk Go, karena kompleksitasnya ekstrem (meskipun lebih kompleks di C++ daripada di Go karena kelebihan metode dan resolusi, yang tidak dimiliki Go).

Setelah mencoba implementasi kontrak saat ini selama sekitar satu bulan, saya sedikit bertanya-tanya seperti apa nasib fungsi bawaan yang ada. Semuanya dapat diimplementasikan secara umum:

func Append(type T)(slice []T, elems ...T) []T {...}
func Copy(type T)(dst, src []T) int {...}
func Delete(type K, V)(m map[K]V, k K) {...}
func Make(type T, I Integer(I))(siz ...I) T {...}
func New(type T)() *T {...}
func Close(type T)(c chan<- T) {...}
func Panic(type T)(v T) {...}
func Recover(type T)() T {...}
func Print(type ...T)(args ...T) {...}
func Println(type ...T)(args ...T) {...}

Apakah mereka akan hilang di Go2? Bagaimana Go 2 dapat menangani dampak yang begitu besar pada basis kode Go 1 yang ada? Ini tampaknya menjadi pertanyaan terbuka.

Selain itu, keduanya sedikit istimewa:

func Len(type T C)(t T) int {...}
func Cap(type T C)(t T) int {...}

Bagaimana menerapkan kontrak seperti itu C dengan desain saat ini, sehingga parameter tipe hanya dapat berupa irisan generik []Ts , map map[Tk]Tv , dan saluran chan Tc di mana T Ts Tk Tv Tc berbeda?

@changkun Saya tidak berpikir "mereka dapat diimplementasikan dengan obat generik" adalah alasan yang meyakinkan untuk menghapusnya. Dan Anda menyebutkan alasan yang cukup jelas dan kuat mengapa mereka tidak boleh dihapus. Jadi saya tidak berpikir mereka akan begitu. Saya pikir itu membuat sisa pertanyaan menjadi usang.

@changkun Saya tidak berpikir "mereka dapat diimplementasikan dengan obat generik" adalah alasan yang meyakinkan untuk menghapusnya. Dan Anda menyebutkan alasan yang cukup jelas dan kuat mengapa mereka tidak boleh dihapus.

Ya, saya setuju bahwa itu bukan yang meyakinkan untuk menghapusnya, itu sebabnya saya mengatakannya secara eksplisit. Namun, menyimpannya bersama dengan obat generik "melanggar" filosofi Go yang ada, yang fitur bahasanya ortogonal. Kompatibilitas adalah perhatian utama, tetapi menambahkan kontrak kemungkinan besar akan mematikan kode "kedaluwarsa" saat ini.

Jadi saya tidak berpikir mereka akan begitu. Saya pikir itu membuat sisa pertanyaan menjadi usang.

Mari kita coba untuk tidak mengabaikan pertanyaan dan menganggapnya sebagai kasus penggunaan kontrak di dunia nyata. Jika ada yang mengajukan persyaratan serupa, bagaimana kami bisa menerapkannya dengan desain saat ini?

Jelas kami tidak akan menyingkirkan fungsi yang sudah dideklarasikan sebelumnya.

Meskipun dimungkinkan untuk menulis tanda tangan fungsi berparameter untuk delete , close , panic , recover , print , dan println , Saya rasa tidak mungkin untuk mengimplementasikannya tanpa bergantung pada fungsi sihir internal.

Ada sebagian versi Append dan Copy di https://go.googlesource.com/proposal/+/refs/heads/master/design/go2draft-contracts.md#append. Itu tidak lengkap, karena append dan copy memiliki kasus khusus untuk argumen kedua bertipe string , yang tidak didukung oleh rancangan desain saat ini.

Perhatikan bahwa tanda tangan untuk Make , di atas, tidak valid menurut rancangan desain saat ini. New tidak persis sama dengan new , tapi, cukup dekat.

Dengan draf desain saat ini Len dan Cap harus mengambil argumen bertipe interface{} , dan dengan demikian tidak akan menjadi compile-time-type-safe.

https://go-review.googlesource.com/c/go/+/187317

Tolong jangan gunakan ekstensi file .go2 , kami memiliki modul untuk melakukan hal versi semacam ini? Saya mengerti jika Anda melakukannya sebagai solusi temporay untuk membuat hidup lebih mudah saat bereksperimen dengan kontrak tetapi harap pastikan bahwa pada akhirnya file go.mod akan berhati-hati mencampur paket go tanpa perlu .go2 ekstensi file. Ini akan menjadi pukulan bagi pengembang modul yang berusaha keras untuk memastikan modul bekerja sebaik mungkin. Menggunakan ekstensi file .go2 go mengatakan, tidak, saya tidak peduli dengan modul Anda, hal-hal akan melakukannya dengan cara saya karena saya tidak ingin kompiler dinosaurus pra modul berusia 10 tahun saya rusak .

@gertcuykens .go2 file hanya untuk percobaan; mereka tidak akan digunakan ketika obat generik mendarat di kompiler.

(Saya akan menyembunyikan komentar kami karena mereka tidak benar-benar menambah diskusi dan cukup lama apa adanya.)

Baru-baru ini saya menjelajahi sintaks generik baru dalam bahasa K yang saya rancang, karena K meminjam banyak tata bahasa dari Go, jadi tata bahasa Generik ini mungkin juga memiliki beberapa nilai referensi untuk Go.

Masalah identifier<T> adalah konflik dengan operator perbandingan dan juga operator bit, jadi saya tidak setuju dengan desain ini.

identifier[T] Scala memiliki tampilan dan nuansa yang lebih baik daripada desain sebelumnya, tetapi setelah menyelesaikan konflik di atas, ia memiliki konflik baru dengan desain indeks identifier[index] .
Untuk alasan ini, desain indeks Scala telah diubah menjadi identifier(index) . Ini tidak berfungsi dengan baik untuk bahasa yang sudah menggunakan [] sebagai indeks.

Dalam draf Go, dinyatakan bahwa obat generik menggunakan (type T) , yang tidak akan menyebabkan konflik, karena type adalah kata kunci, tetapi kompiler masih membutuhkan penilaian lebih ketika dipanggil untuk menyelesaikan identifier(type)(params) . Meskipun lebih baik daripada solusi di atas, itu masih tidak memuaskan saya.

Secara kebetulan, saya ingat desain khusus pemanggilan metode di OC, yang memberi saya inspirasi untuk desain baru.

Bagaimana jika kita menempatkan pengidentifikasi dan generik secara keseluruhan dan memasukkannya ke dalam [] bersama-sama?
Kita bisa mendapatkan [identifier T] . Desain ini tidak bertentangan dengan indeks, karena minimal harus memiliki dua elemen yang dipisahkan oleh spasi.
Ketika ada beberapa generik, kita dapat menulis [identifier T V] seperti ini, dan tidak akan bertentangan dengan desain yang ada.

Mengganti desain ini ke Go, kita bisa mendapatkan contoh berikut.
Misalnya

type [Item T] struct {
    Value T
}

func (it [Item T]) Print() {
    println(it.Value)
}

func [TestGenerics T V]() {
    var a = [Item T]{}
    a.Print()
    var b = [Item V]{}
    b.Print()
}

func main() {
    [TestGenerics int string]()
}

Ini terlihat sangat jelas.

Manfaat lain menggunakan [] adalah ia memiliki beberapa warisan dari desain Slice dan Peta asli Go, dan tidak akan menimbulkan rasa fragmentasi.

[]int  ->  [slice int]

map[string]int  ->  [map string int]

Kita bisa membuat contoh yang lebih rumit

var a map[int][]map[string]map[string][]string

var b [map int [slice [map string [map string [slice string]]]]]

Contoh ini masih mempertahankan efek yang relatif jelas, dan pada saat yang sama memiliki dampak kecil pada kompilasi.

Saya telah menerapkan dan menguji desain ini di K dan berfungsi dengan baik.

Saya pikir desain ini memiliki nilai referensi tertentu dan mungkin layak untuk didiskusikan.

Baru-baru ini saya menjelajahi sintaks generik baru dalam bahasa K yang saya rancang, karena K meminjam banyak tata bahasa dari Go, jadi tata bahasa Generik ini mungkin juga memiliki beberapa nilai referensi untuk Go.

Masalah identifier<T> adalah konflik dengan operator perbandingan dan juga operator bit, jadi saya tidak setuju dengan desain ini.

identifier[T] Scala memiliki tampilan dan nuansa yang lebih baik daripada desain sebelumnya, tetapi setelah menyelesaikan konflik di atas, ia memiliki konflik baru dengan desain indeks identifier[index] .
Untuk alasan ini, desain indeks Scala telah diubah menjadi identifier(index) . Ini tidak berfungsi dengan baik untuk bahasa yang sudah menggunakan [] sebagai indeks.

Dalam draf Go, dinyatakan bahwa obat generik menggunakan (type T) , yang tidak akan menyebabkan konflik, karena type adalah kata kunci, tetapi kompiler masih membutuhkan penilaian lebih ketika dipanggil untuk menyelesaikan identifier(type)(params) . Meskipun lebih baik daripada solusi di atas, itu masih tidak memuaskan saya.

Secara kebetulan, saya ingat desain khusus pemanggilan metode di OC, yang memberi saya inspirasi untuk desain baru.

Bagaimana jika kita menempatkan pengidentifikasi dan generik secara keseluruhan dan memasukkannya ke dalam [] bersama-sama?
Kita bisa mendapatkan [identifier T] . Desain ini tidak bertentangan dengan indeks, karena minimal harus memiliki dua elemen yang dipisahkan oleh spasi.
Ketika ada beberapa generik, kita dapat menulis [identifier T V] seperti ini, dan tidak akan bertentangan dengan desain yang ada.

Mengganti desain ini ke Go, kita bisa mendapatkan contoh berikut.
Misalnya

type [Item T] struct {
    Value T
}

func (it [Item T]) Print() {
    println(it.Value)
}

func [TestGenerics T V]() {
    var a = [Item T]{}
    a.Print()
    var b = [Item V]{}
    b.Print()
}

func main() {
    [TestGenerics int string]()
}

Ini terlihat sangat jelas.

Manfaat lain menggunakan [] adalah ia memiliki beberapa warisan dari desain Slice dan Peta asli Go, dan tidak akan menimbulkan rasa fragmentasi.

[]int  ->  [slice int]

map[string]int  ->  [map string int]

Kita bisa membuat contoh yang lebih rumit

var a map[int][]map[string]map[string][]string

var b [map int [slice [map string [map string [slice string]]]]]

Contoh ini masih mempertahankan efek yang relatif jelas, dan pada saat yang sama memiliki dampak kecil pada kompilasi.

Saya telah menerapkan dan menguji desain ini di K dan berfungsi dengan baik.

Saya pikir desain ini memiliki nilai referensi tertentu dan mungkin layak untuk didiskusikan.

Bagus

Setelah beberapa bolak-balik dan beberapa pembacaan ulang, saya secara keseluruhan mendukung rancangan desain saat ini untuk Kontrak di Go. Saya menghargai jumlah waktu dan upaya yang telah dilakukan. Sementara ruang lingkup, konsep, implementasi, dan sebagian besar pengorbanan tampak masuk akal, kekhawatiran saya adalah bahwa sintaks perlu dirombak untuk meningkatkan keterbacaan.

Saya menulis serangkaian perubahan yang diusulkan untuk mengatasi ini:

Poin-poin penting adalah:

  • Metode Panggilan/Tipe-Tegaskan Sintaks untuk Deklarasi Kontrak
  • "Kontrak Kosong"
  • Pembatas Non-Kurung

Dengan risiko mendahului esai, saya akan memberikan beberapa penjelasan tanpa sintaks, yang dikonversi dari sampel dalam draf desain Kontrak saat ini. Perhatikan bahwa bentuk pembatas F«T» adalah ilustratif, bukan preskriptif; lihat tulisan untuk detailnya.

type List«type Element contract{}» struct {
    next *List«Element»
    val  Element
}

dan

contract viaStrings«To, From» {
    To.Set(string)
    From.String() string
}

func SetViaStrings«type To, From viaStrings»(s []From) []To {
    r := make([]To, len(s))
    for i, v := range s {
        r[i].Set(v.String())
    }
    return r
}

dan

func Keys«type K comparable, V contract{}»(m map[K]V) []K {
    r := make([]K, 0, len(m))
    for k := range m {
        r = append(r, k)
    }
    return r
}

k := maps.Keys(map[int]int{1:2, 2:4})

dan

contract Numeric«T» {
    T.(int, int8, int16, int32, int64,
        uint, uint8, uint16, uint32, uint64, uintptr,
        float32, float64,
        complex64, complex128)
}

func DotProduct«type T Numeric»(s1, s2 []T) T {
    if len(s1) != len(s2) {
        panic("DotProduct: slices of unequal length")
    }
    var r T
    for i := range s1 {
        r += s1[i] * s2[i]
    }
    return r
}

Tanpa benar-benar mengubah Kontrak di bawah tenda, ini jauh lebih mudah dibaca oleh saya sebagai pengembang Go. Saya juga merasa jauh lebih percaya diri mengajarkan formulir ini kepada seseorang yang sedang belajar Go (walaupun terlambat dalam kurikulum).

@ianlancetaylor Berdasarkan komentar Anda di https://github.com/golang/go/issues/36533#issuecomment -579484523 Saya memposting di utas ini daripada memulai masalah baru. Itu juga terdaftar di Halaman Umpan Balik Generik . Tidak yakin apakah saya perlu melakukan hal lain untuk membuatnya "dipertimbangkan secara resmi" (yaitu grup peninjau proposal Go 2 ?) atau jika umpan balik masih dikumpulkan secara aktif.

Dari draft desain kontrak:

Mengapa tidak menggunakan sintaks F<T> seperti C++ dan Java?
Saat mengurai kode dalam suatu fungsi, seperti v := F<T> , pada titik melihat < itu ambigu apakah kita melihat instantiasi tipe atau ekspresi menggunakan operator < . Penyelesaian yang membutuhkan pandangan ke depan yang tidak terbatas secara efektif. Secara umum kami berusaha untuk menjaga parser Go tetap sederhana.

Tidak terlalu bertentangan dengan posting terakhir saya: Pembatas Penjepit Sudut untuk Kontrak Go

Hanya beberapa ide tentang bagaimana menyiasati titik parser ini menjadi bingung. sampel pasangan:

// Lifted from the design draft
func New<type K, V>(compare func(K, K) int) *Map<K, V> {
    return &Map{<K, V> compare: compare}
}

// ...

func (m *Map<K, V>) InOrder() *Iterator<K, V> {
    sender, receiver := chans.Ranger(<keyValue<K, V>>)
    var f func(*node<K, V>) bool
    f = func(n *node<K, V>) bool {
        if n == nil {
            return true
        }
        // Stop sending values if sender.Send returns false,
        // meaning that nothing is listening at the receiver end.
        return f(n.left) &&
            sender.Send(keyValue{<K, V> n.key, n.val}) &&
            f(n.right)
    }
    go func() {
        f(m.root)
        sender.Close()
    }()
    return &Iterator{receiver}
}

// ...

Pada dasarnya, hanya posisi yang berbeda untuk parameter tipe dalam skenario di mana < bisa menjadi ambigu.

@toolbox Mengenai komentar braket sudut Anda. Terima kasih, tetapi bagi saya pribadi sintaks itu berbunyi seperti pertama membuat keputusan bahwa kita harus menggunakan kurung sudut untuk parameter tipe dan argumen tipe dan kemudian mencari cara untuk memasukkannya. Saya pikir jika kita menambahkan generik ke Go, kita perlu membidik untuk sesuatu yang cocok dengan rapi dan mudah ke dalam bahasa yang ada. Saya tidak berpikir bahwa tanda kurung sudut bergerak di dalam tanda kurung kurawal mencapai tujuan itu.

Ya, ini adalah detail kecil, tetapi saya pikir ketika berbicara tentang sintaks, detail kecil sangat penting. Saya pikir jika kita akan menambahkan argumen dan parameter tipe, mereka harus bekerja dengan cara yang sederhana dan intuitif.

Saya tentu tidak mengklaim bahwa sintaks dalam draf desain saat ini sempurna, tetapi saya mengklaim bahwa sintaks itu cocok dengan bahasa yang ada. Yang perlu kita lakukan sekarang adalah menulis lebih banyak kode contoh untuk melihat seberapa baik kerjanya dalam praktik. Poin kuncinya adalah: seberapa sering orang benar-benar harus menulis argumen tipe di luar deklarasi fungsi, dan seberapa membingungkan kasus-kasus itu? Saya tidak berpikir kita tahu.

Apakah ide yang baik untuk menggunakan [] untuk tipe generik, dan menggunakan () untuk fungsi generik? Ini akan lebih konsisten dengan obat generik inti saat ini.

Bisakah masyarakat memilihnya? Secara pribadi saya lebih suka _anything_ daripada menambahkan lebih banyak tanda kurung, sudah sulit untuk membaca beberapa definisi fungsi untuk penutupan dll, ini menambah lebih banyak kekacauan

Saya tidak berpikir pemungutan suara adalah cara yang baik untuk merancang bahasa. Terutama dengan sangat sulit (mungkin tidak mungkin) untuk menentukan dan sangat banyak pemilih yang memenuhi syarat.

Saya percaya para desainer dan komunitas Go untuk bertemu pada solusi terbaik dan
jadi belum merasa perlu untuk mempertimbangkan apa pun dalam percakapan ini.
Namun, saya hanya harus mengatakan betapa tak terduga saya sangat senang dengan
saran sintaks F«T».

(Kurung Unicode lainnya:
https://unicode-search.net/unicode-namesearch.pl?term=BRACKET.)

Bersulang,

  • Bob

Pada Jumat, 1 Mei 2020 pukul 19:43 Matt [email protected] menulis:

Setelah beberapa bolak-balik dan beberapa pembacaan ulang, saya secara keseluruhan mendukung
draft desain saat ini untuk Kontrak di Go. Saya menghargai jumlah waktu
dan usaha yang telah dilakukan. Sedangkan ruang lingkup, konsep,
implementasi, dan sebagian besar pengorbanan tampak masuk akal, kekhawatiran saya adalah bahwa
sintaks perlu dirombak untuk meningkatkan keterbacaan.

Saya menulis serangkaian perubahan yang diusulkan untuk mengatasi ini:

Poin-poin penting adalah:

  • Metode Panggilan/Tipe-Tegaskan Sintaks untuk Deklarasi Kontrak
  • "Kontrak Kosong"
  • Pembatas Non-Kurung

Dengan risiko mendahului esai, saya akan memberikan beberapa bagian yang tidak didukung
sintaks, dikonversi dari sampel dalam rancangan desain Kontrak saat ini. Catatan
bahwa bentuk pembatas F«T» bersifat ilustratif, bukan preskriptif; melihat
penulisan untuk detailnya.

type List«type Element contract{}» struct {
berikutnya *Daftar«Elemen»
elemen val
}

dan

kontrak melaluiString«Ke, Dari» {
Untuk.Set(string)
Dari.String() string
}
func SetViaStrings«ketik Ke, Dari viaStrings»(s []Dari) []Ke {
r := buat([]Kepada, len(s))
untuk i, v := rentang s {
r[i].Set(v.String())
}
kembali r
}

dan

func Keys«tipe K sebanding, kontrak V{}»(m peta[K]V) []K {
r := buat([]K, 0, len(m))
untuk k := rentang m {
r = tambahkan(r, k)
}
kembali r
}
k := maps.Keys(map[int]int{1:2, 2:4})

dan

kontrak Numerik«T» {
T.(int, int8, int16, int32, int64,
uint, uint8, uint16, uint32, uint64, uintptr,
float32, float64,
kompleks64, kompleks128)
}
func Produk Dot«tipe T Numerik»(s1, s2 []T) T {
if len(s1) != len(s2) {
panic("DotProduct: irisan dengan panjang yang tidak sama")
}
var r T
untuk i := rentang s1 {
r += s1[i] * s2[i]
}
kembali r
}

Tanpa benar-benar mengubah Kontrak di bawah tenda, ini jauh lebih
dapat dibaca oleh saya sebagai pengembang Go. Saya juga merasa jauh lebih percaya diri
mengajarkan formulir ini kepada seseorang yang sedang belajar Go (walaupun terlambat di
kurikulum).

@ianlancetaylor https://github.com/ianlancetaylor Berdasarkan komentar Anda
di #36533 (komentar)
https://github.com/golang/go/issues/36533#issuecomment-579484523 Saya
posting di utas ini daripada memulai masalah baru. Itu juga terdaftar
di Halaman Umpan Balik Generik
https://github.com/golang/go/wiki/Go2GenericsFeedback . Tidak yakin apakah saya
perlu melakukan hal lain untuk membuatnya "dipertimbangkan secara resmi" (yaitu Go 2
grup peninjau proposal https://github.com/golang/go/issues/33892 ?) atau jika
umpan balik masih aktif dikumpulkan.


Anda menerima ini karena Anda berlangganan utas ini.
Balas email ini secara langsung, lihat di GitHub
https://github.com/golang/go/issues/15292#issuecomment-622657596 , atau
berhenti berlangganan
https://github.com/notifications/unsubscribe-auth/AACQ2NJRBNLLDGY2XGCCQCLRPOCEHANCNFSM4CA35RXQ
.

Kita semua menginginkan sintaks terbaik untuk Go. Draf desain menggunakan tanda kurung karena bekerja dengan sisa Go tanpa menyebabkan ambiguitas penguraian yang signifikan. Kami tinggal bersama mereka karena mereka adalah solusi terbaik dalam pikiran kami saat itu dan karena ada ikan yang lebih besar untuk digoreng. Sejauh ini mereka (tanda kurung) telah bertahan dengan cukup baik.

Pada akhirnya, jika notasi yang jauh lebih baik ditemukan, itu sangat mudah diubah selama kita tidak memiliki jaminan kompatibilitas untuk dipatuhi (parser disesuaikan secara sepele, dan setiap badan kode dapat dikonversi mudah dengan gofmt).

@ianlancetaylor Terima kasih atas jawabannya, ini dihargai.

Kamu benar; sintaks itu adalah "jangan gunakan tanda kurung untuk argumen tipe" dan pilih apa yang menurut saya kandidat terbaik, lalu buat perubahan untuk mencoba meringankan masalah implementasi dengan parser.

Jika sintaksnya sulit dibaca, (sulit untuk mengetahui apa yang terjadi secara sekilas) apakah itu benar-benar cocok dengan bahasa yang ada? Di situlah saya pikir sikapnya gagal.

Memang benar, saat Anda menyentuhnya, inferensi tipe itu bisa sangat mengurangi jumlah argumen tipe yang perlu diteruskan dalam kode klien. Saya pribadi percaya bahwa penulis perpustakaan harus berusaha untuk meminta argumen tipe nol untuk diteruskan saat menggunakan kode mereka, namun itu akan terjadi dalam praktik.

Tadi malam, secara kebetulan, saya menemukan sintaks template untuk D yang secara mengejutkan mirip dalam beberapa hal:

template Square(T) {
    T Square(T t) {
        return t * t;
    }
}

writefln("The square of %s is %s", 3, Square!(int)(3));

template TCopy(T) {
    void copy(out T to, T from) {
        to = from;
    }
}

int i;
TCopy!(int).copy(i, 3);

Ada dua perbedaan utama yang saya lihat:

  1. Mereka memiliki ! sebagai operator instantiasi untuk menggunakan template.
  2. Gaya deklarasi mereka (tidak ada beberapa nilai kembalian, metode bersarang di kelas) berarti bahwa ada lebih sedikit tanda kurung dalam kode biasa, jadi menggunakan tanda kurung untuk parameter tipe tidak menciptakan ambiguitas visual yang sama.

Operator Instansiasi

Saat menggunakan Kontrak, ambiguitas visual utama adalah antara instantiasi dan panggilan fungsi (atau konversi tipe, atau...?). Bagian dari mengapa ini bermasalah adalah bahwa instantiasi adalah waktu kompilasi dan panggilan fungsi adalah waktu proses. Go memiliki banyak petunjuk visual yang memberi tahu pembaca kamp mana yang dimiliki setiap klausa, tetapi sintaks baru mengacaukannya, jadi tidak jelas apakah Anda melihat jenis atau alur program.

Salah satu contoh yang dibuat-buat:

// Instantiation with unexported types and then function call,
// or chained method call?
a := draw(square, ellipse)(canvas, color)

Proposal: gunakan operator instantiasi untuk menentukan parameter tipe. ! yang digunakan D tampaknya sangat dapat diterima. Beberapa contoh sintaks:

// Lifted from the design draft
func New(type K, V)(compare func(K, K) int) *Map!(K, V) {
    return &Map!(K, V){compare: compare}
}

// ...

func (m *Map(K, V)) InOrder() *Iterator!(K, V) {
    sender, receiver := chans.Ranger!(keyValue!(K, V))()
    var f func(*node!(K, V)) bool
    f = func(n *node!(K, V)) bool {
        if n == nil {
            return true
        }
        // Stop sending values if sender.Send returns false,
        // meaning that nothing is listening at the receiver end.
        return f(n.left) &&
            sender.Send(keyValue!(K, V){n.key, n.val}) &&
            f(n.right)
    }
    go func() {
        f(m.root)
        sender.Close()
    }()
    return &Iterator{receiver}
}

// ...

Dari sudut pandang pribadi saya, kode di atas adalah urutan besarnya lebih mudah dibaca. Saya pikir itu menghapus semua ambiguitas, baik secara visual maupun untuk parser. Selanjutnya, saya bertanya-tanya apakah ini mungkin satu-satunya perubahan terpenting yang dapat dilakukan pada Kontrak.

Gaya Deklarasi

Saat mendeklarasikan tipe dan fungsi serta metode, ada lebih sedikit "run-time atau compile-time?" masalah. Seorang Gopher melihat garis yang dimulai dengan type atau func dan mengetahui bahwa dia sedang melihat deklarasi, bukan perilaku program.

Namun, beberapa ambiguitas visual masih ada:

// Type-parameterized function,
// or function with multiple return values?
func Draw(cvs canvas, t tool)(canvas, tool) {
    // ...
}
func Draw(type canvas, tool)(cvs canvas, t tool) {
    // ...
}

// Type-parameterized struct, or function call?
func Set(elem constructible) rect {
    // ...
}
type Set(type Elem comparable) struct{
    // ...
}

// Method call, or type-parameterized function?
func Map(type Element)(s []Element, f func(Element) Element) (results []Element) {
    // ...
}
func (t Element) Map(s []Element, f func(Element) Element) (results []Element) {
    // ...
}

Pikiran:

  • Saya pikir masalah ini kurang penting daripada masalah instantiasi.
  • Solusi yang paling jelas adalah mengubah pembatas yang digunakan untuk argumen tipe.
  • Mungkin menempatkan beberapa jenis operator atau karakter lain di sana ( ! mungkin tersesat, bagaimana dengan # ?) dapat mengaburkan hal-hal.

EDIT: @griesemer terima kasih atas klarifikasi tambahannya!

Terima kasih. Hanya untuk mengajukan pertanyaan alami: mengapa penting untuk mengetahui apakah panggilan tertentu dievaluasi pada waktu berjalan atau pada waktu kompilasi? Mengapa itu pertanyaan kuncinya?

@toolbox

// Instantiation with unexported types and then function call,
// or chained method call?
a := draw(square, ellipse)(canvas, color)

Mengapa itu penting? Untuk pembaca biasa, tidak masalah jika ini adalah bagian dari kode yang dieksekusi selama waktu kompilasi atau runtime. Untuk semua orang, mereka hanya dapat melihat definisi fungsi untuk mengetahui apa yang sedang terjadi. Contoh Anda nanti tampaknya tidak ambigu sama sekali.

Sebenarnya, menggunakan () untuk parameter tipe masuk akal, karena sepertinya Anda memanggil fungsi yang mengembalikan fungsi - dan itu kurang lebih benar. Perbedaannya adalah bahwa fungsi pertama adalah menerima tipe, yang biasanya menggunakan huruf besar, atau sangat terkenal.

Pada tahap ini, jauh lebih penting untuk mengetahui dimensi gudang, bukan warnanya.

Saya tidak berpikir apa yang @toolbox bicarakan benar-benar perbedaan antara waktu kompilasi dan run-time. Ya itu salah satu perbedaannya, tapi bukan itu yang penting. Yang penting adalah: apakah ini pemanggilan fungsi atau deklarasi tipe? Anda ingin tahu karena mereka berperilaku berbeda dan Anda tidak ingin harus menyimpulkan apakah beberapa ekspresi membuat dua panggilan fungsi atau satu, karena itu perbedaan besar. Yaitu ekspresi seperti a := draw(square, ellipse)(canvas, color) ambigu tanpa melakukan pekerjaan untuk memeriksa lingkungan sekitarnya.

Mampu menguraikan aliran kontrol program secara visual adalah penting. Saya pikir Go telah menjadi contoh yang bagus untuk ini.

Terima kasih. Hanya untuk mengajukan pertanyaan alami: mengapa penting untuk mengetahui apakah panggilan tertentu dievaluasi pada waktu berjalan atau pada waktu kompilasi? Mengapa itu pertanyaan kuncinya?

Maaf, sepertinya saya mengacaukan komunikasi saya. Ini adalah poin kunci yang saya coba sampaikan:

tidak jelas jika Anda melihat jenis atau aliran program

(Saat ini, satu diurutkan selama kompilasi dan yang lainnya terjadi saat run-time, tetapi itu adalah...karakteristik, bukan poin kunci, yang diambil dengan benar oleh @infogulch —terima kasih!)


Saya telah melihat pendapat di beberapa tempat bahwa obat generik dalam draf dapat disamakan dengan panggilan fungsi: ini adalah semacam fungsi waktu kompilasi yang mengembalikan fungsi atau tipe sebenarnya . Meskipun itu membantu sebagai model mental tentang apa yang terjadi selama kompilasi, itu tidak diterjemahkan secara sintaksis. Secara sintaksis, mereka kemudian harus dinamai seperti fungsi. Berikut ini contohnya:

// Example from the Contracts draft
Print(int)([]int{1, 2, 3})

// New naming that communicates behavior and intent
MakePrintFunc(int)([]int{1, 2, 3}) // Chained function call, great!

Di sana, itu benar-benar terlihat seperti fungsi yang mengembalikan fungsi; Saya pikir itu cukup mudah dibaca.

Cara lain untuk melakukannya adalah dengan menambahkan semuanya dengan Type , jadi jelas dari namanya bahwa ketika Anda "memanggil" fungsi, Anda mendapatkan tipe. Jika tidak, tidak jelas bahwa (misalnya) Pair(...) menghasilkan tipe struct daripada struct. Tetapi jika konvensi itu ada, kode ini menjadi jelas: a := drawType(square, ellipse)(canvas, color)

(Saya menyadari bahwa preseden adalah konvensi "-er" untuk antarmuka.)

Perhatikan bahwa saya tidak secara khusus mendukung hal di atas sebagai solusi, saya hanya mengilustrasikan bagaimana menurut saya "generik sebagai fungsi" tidak sepenuhnya dan jelas diungkapkan oleh sintaks saat ini.


Sekali lagi, @infogulch telah merangkum poin saya dengan sangat baik. Saya mendukung argumen tipe yang membedakan secara visual sehingga jelas mereka adalah bagian dari tipe .

Mungkin bagian visualnya akan ditingkatkan dengan penyorotan sintaks editor.

Saya tidak tahu banyak tentang parser dan bagaimana Anda tidak bisa terlalu banyak melihat ke depan.

Dari sudut pandang pengguna, saya tidak ingin melihat karakter lain lagi dalam kode saya, jadi «» tidak akan mendapatkan dukungan saya (saya tidak menemukannya di keyboard saya!).

Namun, melihat tanda kurung bulat diikuti oleh tanda kurung bulat juga tidak enak dipandang.

Bagaimana kalau sederhana menggunakan kurung kurawal?

a := draw{square, ellipse}(canvas, color)

Dalam Print(int)([]int{1,2,3}) satu-satunya perbedaan perilaku adalah "waktu kompilasi vs. run-time", meskipun. Ya, MakePrintFunc bukannya Print akan lebih menekankan kesamaan ini, tapi… bukankah itu argumen untuk tidak menggunakan MakePrintFunc ? Karena itu sebenarnya menyembunyikan perbedaan perilaku yang sebenarnya.

FWIW, jika Anda tampaknya membuat argumen untuk menggunakan pemisah yang berbeda untuk fungsi parametrik dan tipe parametrik. Karena Print(int) sebenarnya dapat dianggap setara dengan fungsi yang mengembalikan fungsi (dievaluasi pada waktu kompilasi), sedangkan Pair(int, string) tidak bisa - ini adalah fungsi yang mengembalikan tipe . Print(int) sebenarnya adalah ekspresi valid yang dievaluasi ke func -nilai, sedangkan Pair(int, string) bukan ekspresi yang valid, ini adalah spesifikasi tipe. Jadi perbedaan nyata dalam penggunaan bukanlah "fungsi generik vs. non-generik" melainkan "fungsi generik vs. tipe generik". Dan dari POV itu, saya pikir ada alasan kuat yang harus dibuat untuk menggunakan () setidaknya untuk fungsi parametrik, karena itu menekankan sifat fungsi parametrik untuk benar-benar mewakili nilai - dan mungkin kita harus menggunakan <> untuk tipe parametrik.

Saya pikir argumen untuk () untuk tipe parametrik berasal dari pemrograman fungsional, di mana tipe-fungsi-pengembalian ini adalah konsep nyata yang disebut konstruktor Tipe dan sebenarnya dapat digunakan dan dirujuk sebagai fungsi. Dan FWIW, itu juga mengapa saya tidak akan berdebat untuk tidak menggunakan () untuk tipe parametrik. Secara pribadi, saya sangat nyaman dengan konsep ini dan saya lebih suka keuntungan dari lebih sedikit pemisah yang berbeda, daripada keuntungan dari disambiguasi fungsi parametrik dari tipe parametrik - lagi pula, kami tidak memiliki masalah dengan pengidentifikasi murni yang mengacu pada salah satu jenis atau nilai juga .

Saya tidak berpikir apa yang @toolbox bicarakan benar-benar perbedaan antara waktu kompilasi dan run-time. Ya itu salah satu perbedaannya, tapi bukan itu yang penting. Yang penting adalah: apakah ini pemanggilan fungsi atau deklarasi tipe? Anda _ingin_ tahu karena mereka berperilaku berbeda dan Anda tidak ingin harus menyimpulkan apakah beberapa ekspresi membuat dua panggilan fungsi atau satu, karena itu perbedaan besar. Yaitu ekspresi seperti a := draw(square, ellipse)(canvas, color) ambigu tanpa melakukan pekerjaan untuk memeriksa lingkungan sekitarnya.

Mampu menguraikan aliran kontrol program secara visual adalah penting. Saya pikir Go telah menjadi contoh yang bagus untuk ini.

Deklarasi tipe akan sangat mudah dilihat, karena semuanya dimulai dengan kata kunci type . Contoh Anda jelas bukan salah satunya.

Mungkin bagian visualnya akan ditingkatkan dengan penyorotan sintaks editor.

Saya pikir, idealnya, sintaks harus jelas apa pun warnanya. Itulah yang terjadi pada Go, dan menurut saya tidak baik untuk turun dari standar itu.

Bagaimana kalau sederhana menggunakan kurung kurawal?

Saya percaya ini sayangnya bertentangan dengan struct literal.

Dalam Print(int)([]int{1,2,3}) satu-satunya perbedaan perilaku adalah "waktu kompilasi vs. run-time", meskipun. Ya, MakePrintFunc bukannya Print akan lebih menekankan kesamaan ini, tapi… bukankah itu argumen untuk tidak menggunakan MakePrintFunc ? Karena itu sebenarnya menyembunyikan perbedaan perilaku yang sebenarnya.

Nah, untuk satu, inilah mengapa saya akan mendukung Print!(int)([]int{1,2,3}) lebih dari MakePrintFunc(int)([]int{1,2,3}) . Jelas bahwa sesuatu yang unik sedang terjadi.

Tetapi sekali lagi, pertanyaan yang diajukan oleh @ianlancetaylor sebelumnya: mengapa penting jika tipe instantiasi/fungsi-pengembalian-fungsi adalah waktu kompilasi vs. run-time?

Memikirkannya, jika Anda menulis beberapa panggilan fungsi dan kompiler dapat mengoptimalkannya dan menghitung hasilnya pada waktu kompilasi, Anda akan senang dengan peningkatan kinerja! Sebaliknya, aspek penting adalah apa yang dilakukan kode, apa perilakunya? Itu harus jelas dalam sekejap.

Ketika saya melihat Print(...) insting pertama saya adalah "itu adalah panggilan fungsi yang menulis ke suatu tempat". Itu tidak berkomunikasi "ini akan mengembalikan fungsi". Menurut pendapat saya, semua ini lebih baik karena dapat mengomunikasikan perilaku dan niat:

  • MakePrintFunc(...)
  • Print!(...)
  • Print<...>

Dengan kata lain, potongan kode ini "referensi" atau dalam beberapa cara "memberi saya" fungsi yang sekarang dapat dipanggil dalam bit kode berikut.

FWIW, jika Anda tampaknya membuat argumen untuk menggunakan pemisah yang berbeda untuk fungsi parametrik dan tipe parametrik. ...

Tidak, saya tahu bahwa beberapa contoh terakhir adalah tentang fungsi, tetapi saya akan menganjurkan sintaks yang konsisten untuk fungsi parametrik dan tipe parametrik. Saya tidak percaya Tim Go akan menambahkan Generik ke dalam Go kecuali mereka adalah konsep terpadu dengan sintaksis terpadu.

Ketika saya melihat Print(...) insting pertama saya adalah "itu adalah panggilan fungsi yang menulis ke suatu tempat". Itu tidak berkomunikasi "ini akan mengembalikan fungsi".

Juga tidak func Print(…) func(…) , ketika dipanggil sebagai Print(…) . Namun, kami secara kolektif baik-baik saja dengan itu. Tanpa sintaks panggilan khusus, jika suatu fungsi mengembalikan func .
Sintaks Print(…) memberi tahu Anda persis apa yang dilakukannya hari ini: Print itu adalah fungsi yang mengembalikan beberapa nilai, yang dievaluasi oleh Print(…) . Jika Anda tertarik dengan tipe yang dikembalikan oleh fungsi, lihat definisinya.
Atau, jauh lebih mungkin, gunakan fakta bahwa sebenarnya Print(…)(…) sebagai indikator bahwa ia mengembalikan suatu fungsi.

Memikirkannya, jika Anda menulis beberapa panggilan fungsi dan kompiler dapat mengoptimalkannya dan menghitung hasilnya pada waktu kompilasi, Anda akan senang dengan peningkatan kinerja!

Tentu. Kami sudah memiliki itu. Dan saya sangat senang bahwa saya tidak memerlukan anotasi sintaksis khusus untuk membuatnya istimewa, tetapi hanya dapat percaya bahwa kompiler akan memberikan heuristik yang terus ditingkatkan untuk fungsi mana saja.

Menurut pendapat saya, semua ini lebih baik karena dapat mengomunikasikan perilaku dan niat:

Perhatikan bahwa yang pertama setidaknya 100% kompatibel dengan desain. Itu tidak meresepkan bentuk apa pun untuk pengidentifikasi yang digunakan dan saya harap Anda tidak menyarankan untuk meresepkannya (dan jika Anda melakukannya, saya akan tertarik mengapa aturan yang sama tidak berlaku untuk hanya mengembalikan func ).

Tidak, saya tahu bahwa beberapa contoh terakhir adalah tentang fungsi, tetapi saya akan menganjurkan sintaks yang konsisten untuk fungsi parametrik dan tipe parametrik.

Yah, saya setuju, seperti yang saya katakan :) Saya hanya mengatakan bahwa saya tidak mengerti bagaimana argumen yang Anda buat dapat diterapkan di sepanjang sumbu "generik vs. non-generik", karena tidak ada perubahan perilaku penting antara dua. Mereka akan masuk akal di sepanjang sumbu "tipe vs. fungsi", karena apakah sesuatu adalah spesifikasi tipe atau ekspresi sangat penting untuk konteks yang dapat digunakan. Saya masih tidak akan setuju, tetapi setidaknya saya akan mengerti mereka :)

@Merovius terima kasih atas komentar Anda.

Juga tidak func Print(…) func(…) , ketika dipanggil sebagai Print(…) . Namun, kami secara kolektif baik-baik saja dengan itu. Tanpa sintaks panggilan khusus, jika suatu fungsi mengembalikan fungsi.
Sintaks Print(…) memberi tahu Anda persis apa yang dilakukannya hari ini: Print itu adalah fungsi yang mengembalikan beberapa nilai, yang dievaluasi oleh Print(…) . Jika Anda tertarik dengan tipe yang dikembalikan oleh fungsi, lihat definisinya.

Saya berpandangan bahwa nama fungsi harus terkait dengan fungsinya. Oleh karena itu saya berharap Print(...) untuk mencetak sesuatu, terlepas dari apa yang dikembalikan. Saya percaya ini adalah harapan yang masuk akal, dan harapan yang dapat ditemukan terpenuhi di sebagian besar kode Go yang ada.

Jika saya melihat Print(...)(...) itu mengomunikasikan bahwa () pertama telah mencetak sesuatu, dan bahwa fungsi tersebut telah mengembalikan suatu fungsi, dan () kedua menjalankan perilaku tambahan itu .

(Saya akan terkejut jika ini adalah pendapat yang tidak biasa atau langka, tetapi saya tidak akan membantah beberapa hasil survei.)

Perhatikan bahwa yang pertama setidaknya 100% kompatibel dengan desain. Itu tidak meresepkan bentuk apa pun untuk pengidentifikasi yang digunakan dan saya harap Anda tidak menyarankan untuk meresepkannya (dan jika Anda melakukannya, saya akan tertarik mengapa aturan yang sama tidak berlaku hanya untuk mengembalikan fungsi).

Anda benar, saya menyarankan itu :)

Dengar, saya membuat daftar 3 cara yang dapat saya pikirkan untuk memperbaiki ambiguitas visual yang diperkenalkan oleh params tipe pada fungsi dan tipe. Jika Anda tidak melihat ambiguitas, maka Anda tidak akan menyukai saran apa pun!

Saya hanya mengatakan bahwa saya tidak mengerti bagaimana argumen yang Anda buat dapat diterapkan di sepanjang sumbu "generik vs. non-generik", karena tidak ada perubahan perilaku yang penting di antara keduanya. Mereka akan masuk akal di sepanjang sumbu "tipe vs. fungsi", karena apakah sesuatu adalah spesifikasi tipe atau ekspresi sangat penting untuk konteks yang dapat digunakan.

Lihat poin di atas tentang ambiguitas dan 3 solusi yang diusulkan.

Parameter tipe adalah hal baru.

  • Jika kita ingin mempertimbangkannya sebagai hal baru, maka saya mengusulkan untuk mengubah pembatas atau menambahkan operator instantiasi untuk sepenuhnya membedakannya dari kode biasa: panggilan fungsi, konversi jenis, dll.
  • Jika kita ingin alasan tentang mereka hanya sebagai fungsi lain maka saya mengusulkan penamaan fungsi tersebut dengan jelas, sehingga identifier di identifier(...) mengkomunikasikan perilaku dan nilai kembalian.

Saya lebih suka yang pertama. Dalam kedua kasus, perubahan akan bersifat global di seluruh sintaks parameter tipe, seperti yang dibahas.

Ada beberapa cara lain untuk menjelaskan hal ini:

  1. survei
  2. tutorial

1. Survei

Kata Pengantar: Ini bukan demokrasi. Saya benar-benar berpikir bahwa keputusan didasarkan pada data, dan logika yang diartikulasikan dan data survei yang luas dapat membantu proses keputusan.

Saya tidak memiliki sarana untuk melakukan ini, tetapi saya akan tertarik untuk mengetahui apa yang akan terjadi jika Anda mensurvei beberapa ribu Gophers pada "peringkat ini dengan kejelasan".

Dasar:

// Lifted from the design draft
func New(type K, V)(compare func(K, K) int) *Map(K, V) {
    return &Map(K, V){compare: compare}
}

// ...

func (m *Map(K, V)) InOrder() *Iterator(K, V) {
    sender, receiver := chans.Ranger(keyValue(K, V))()
    var f func(*node(K, V)) bool
    f = func(n *node(K, V)) bool {
        if n == nil {
            return true
        }
        // Stop sending values if sender.Send returns false,
        // meaning that nothing is listening at the receiver end.
        return f(n.left) &&
            sender.Send(keyValue(K, V){n.key, n.val}) &&
            f(n.right)
    }
    go func() {
        f(m.root)
        sender.Close()
    }()
    return &Iterator{receiver}
}

// ...

Operator instantiasi:

// Lifted from the design draft
func New(type K, V)(compare func(K, K) int) *Map!(K, V) {
    return &Map!(K, V){compare: compare}
}

// ...

func (m *Map(K, V)) InOrder() *Iterator!(K, V) {
    sender, receiver := chans.Ranger!(keyValue!(K, V))()
    var f func(*node!(K, V)) bool
    f = func(n *node!(K, V)) bool {
        if n == nil {
            return true
        }
        // Stop sending values if sender.Send returns false,
        // meaning that nothing is listening at the receiver end.
        return f(n.left) &&
            sender.Send(keyValue!(K, V){n.key, n.val}) &&
            f(n.right)
    }
    go func() {
        f(m.root)
        sender.Close()
    }()
    return &Iterator{receiver}
}

// ...

Kawat gigi sudut: (atau kawat gigi sudut ganda, dengan cara apa pun)

// Lifted from the design draft
func New<type K, V>(compare func(K, K) int) *Map<K, V> {
    return &Map<K, V>{compare: compare}
}

// ...

func (m *Map<K, V>) InOrder() *Iterator<K, V> {
    sender, receiver := chans.Ranger<keyValue<K, V>>()
    var f func(*node<K, V>) bool
    f = func(n *node<K, V>) bool {
        if n == nil {
            return true
        }
        // Stop sending values if sender.Send returns false,
        // meaning that nothing is listening at the receiver end.
        return f(n.left) &&
            sender.Send(keyValue<K, V>{n.key, n.val}) &&
            f(n.right)
    }
    go func() {
        f(m.root)
        sender.Close()
    }()
    return &Iterator{receiver}
}

// ...

Fungsi yang dinamai dengan tepat:

// Lifted from the design draft
func NewConstructor(type K, V)(compare func(K, K) int) *MapType(K, V) {
    return &MapType(K, V){compare: compare}
}

// ...

func (m *MapType(K, V)) InOrder() *IteratorType(K, V) {
    sender, receiver := chans.RangerType(keyValueType(K, V))()
    var f func(*nodeType(K, V)) bool
    f = func(n *nodeType(K, V)) bool {
        if n == nil {
            return true
        }
        // Stop sending values if sender.Send returns false,
        // meaning that nothing is listening at the receiver end.
        return f(n.left) &&
            sender.Send(keyValueType(K, V){n.key, n.val}) &&
            f(n.right)
    }
    go func() {
        f(m.root)
        sender.Close()
    }()
    return &Iterator{receiver}
}

// ...

...Lucu, sebenarnya aku cukup suka yang terakhir.

(Menurut Anda bagaimana ini akan berjalan di dunia luas Gophers @Merovius ?)

2. Tutorial

Saya pikir ini akan menjadi latihan yang sangat berguna: tulis tutorial ramah-pemula untuk sintaks favorit Anda, dan mintalah beberapa orang membacanya dan menerapkannya. Seberapa mudah konsep dikomunikasikan? Apa FAQ dan bagaimana Anda menjawabnya?

Draf desain dimaksudkan untuk mengkomunikasikan konsep tersebut kepada Gophers yang berpengalaman. Ini mengikuti rantai logika, mencelupkan Anda perlahan-lahan. Apa versi ringkasnya? Bagaimana Anda menjelaskan Aturan Emas Kontrak dalam satu posting blog yang mudah diasimilasi?

Ini bisa menyajikan semacam sudut atau potongan data yang berbeda dari laporan umpan balik biasa.

@toolbox Saya pikir apa yang belum Anda jawab adalah: Mengapa ini menjadi masalah untuk fungsi parametrik, tetapi tidak untuk fungsi non-parametrik yang mengembalikan func ? Saya bisa, hari ini, menulis

func Print(a string) func(string) {
    return func(b string) {
        fmt.Println(a+b)
    }
}

func main() {
    Print("foo")("bar")
}

Mengapa ini baik-baik saja dan tidak membuat Anda menjadi sangat bingung dengan ambiguitas, tetapi segera setelah Print mengambil parameter tipe alih-alih parameter nilai, ini menjadi tak tertahankan? Dan apakah Anda (mengesampingkan pertanyaan kompatibilitas yang jelas) juga menyarankan agar kami menambahkan batasan agar berjalan dengan benar, bahwa ini tidak boleh dilakukan, kecuali Print diubah namanya menjadi MakeXFunc untuk beberapa X ? Jika tidak, mengapa tidak?

@toolbox akankah ini benar-benar menjadi masalah ketika asumsinya adalah bahwa inferensi tipe mungkin sangat baik menghilangkan kebutuhan untuk menentukan tipe parametrik untuk fungsi, hanya menyisakan panggilan fungsi yang tampak sederhana?

@Merovius Saya tidak berpikir masalahnya adalah dengan sintaks Print("foo")("bar") itu sendiri, karena itu sudah mungkin di Go 1, justru karena memiliki satu kemungkinan interpretasi . Masalahnya adalah bahwa dengan proposal yang tidak dimodifikasi, ekspresi Foo(X)(Y) sekarang menjadi ambigu dan bisa berarti Anda membuat dua panggilan fungsi (seperti di Go 1), atau itu bisa berarti Anda membuat satu panggilan fungsi dengan argumen tipe . Masalahnya adalah kemampuan untuk menyimpulkan secara lokal apa yang dilakukan program, dan dua kemungkinan interpretasi semantik itu sangat berbeda .

@urandom Saya setuju bahwa inferensi tipe mungkin dapat menghilangkan sebagian besar parameter tipe yang disediakan secara eksplisit, tetapi saya tidak berpikir bahwa memasukkan semua kompleksitas kognitif ke sudut gelap bahasa hanya karena mereka jarang digunakan adalah ide yang bagus salah satu. Bahkan jika itu cukup langka sehingga kebanyakan orang biasanya tidak menemukannya, mereka kadang-kadang masih akan menemukannya, dan membiarkan beberapa kode memiliki aliran kontrol yang membingungkan selama itu bukan kode "kebanyakan" meninggalkan rasa tidak enak di mulut saya. Terutama karena Go saat ini sangat mudah didekati ketika membaca kode "pipa ledeng" termasuk stdlib. Mungkin inferensi tipe sangat bagus sehingga "langka" menjadi "tidak pernah", dan pemrogram Go tetap disiplin tinggi dan tidak pernah mendesain sistem di mana parameter tipe diperlukan; maka seluruh masalah ini pada dasarnya diperdebatkan. Tapi saya tidak akan bertaruh untuk itu.

Saya pikir dorongan utama dari argumen @toolbox adalah bahwa kita tidak boleh membebani sintaks yang ada dengan semantik konteks-sensitif, dan sebaliknya kita harus menemukan beberapa sintaks lain yang tidak ambigu (Bahkan jika itu hanya membuat tambahan kecil seperti Foo(X)!(Y) .) Saya pikir ini adalah ukuran penting ketika mempertimbangkan opsi sintaks.

Saya menggunakan dan membaca sedikit kode D , pada masa itu (~2008-2009), dan saya harus mengatakan bahwa ! selalu membuat saya tersandung.

izinkan saya mengecat gudang ini dengan # , $ atau @ , sebagai gantinya (karena mereka tidak memiliki arti dalam Go atau C).
ini kemudian dapat membuka kemungkinan untuk menggunakan kurung kurawal tanpa kebingungan dengan peta, irisan atau struct.

  • Foo@{X}(Y)
  • Foo${X}(Y)
  • Foo#{X}(Y)
    atau kurung siku.

Dalam diskusi seperti ini, penting untuk melihat kode yang sebenarnya.

Misalnya, pertimbangkan bahwa hanya sedikit orang yang menulis Foo(X)(Y) . Di Go, nama jenis dan nama variabel serta nama fungsi terlihat persis sama, namun orang jarang bingung tentang apa yang mereka lihat. Orang-orang memahami bahwa int64(v) adalah konversi tipe dan F(v) adalah pemanggilan fungsi, meskipun tampilannya persis sama.

Kita perlu melihat kode nyata untuk melihat apakah argumen tipe benar-benar membingungkan dalam praktiknya. Jika ya, maka kita harus menyesuaikan sintaksnya. Dengan tidak adanya kode nyata, kita tidak tahu.

Pada Rabu, 6 Mei 2020, pukul 13:00, Ian Lance Taylor menulis:

Orang-orang memahami bahwa int64(v) adalah konversi tipe dan F(v) adalah a
panggilan fungsi, meskipun mereka terlihat persis sama.

Saya tidak punya pendapat satu atau lain cara sekarang pada proposal
sintaks, tapi saya rasa contoh khusus ini tidak terlalu bagus. Mungkin
benar untuk tipe bawaan, tetapi saya sebenarnya bingung dengan ini
masalah yang tepat beberapa kali sendiri (saya sedang mencari fungsi
definisi dan menjadi sangat bingung tentang cara kerja kode sebelumnya
Saya menyadari itu kemungkinan tipe dan saya tidak dapat menemukan fungsinya karena
itu sama sekali bukan panggilan fungsi). Bukan akhir dunia, dan
mungkin tidak masalah sama sekali bagi orang yang menyukai IDE mewah, tapi saya sudah
menyia-nyiakan 5 menit atau lebih untuk melakukan ini beberapa kali.

—Sam

--
Sam Whited

@ianlancetaylor satu hal yang saya perhatikan dari contoh Anda adalah Anda dapat menulis fungsi yang mengambil tipe dan mengembalikan tipe lain dengan arti yang sama, jadi memanggil tipe sebagai konversi tipe dasar seperti int64(v) masuk akal di cara yang sama bahwa strconv.Atoi(v) masuk akal.

Tetapi sementara Anda dapat melakukan UseConverter(strconv.Atoi) , UseConverter(int64) tidak dimungkinkan di Go 1. Memiliki tanda kurung untuk parameter tipe mungkin membuka beberapa kemungkinan jika generik dapat digunakan untuk casting seperti:

func StrToNumber(type K)(s string) K {
  asInt := strconb.Atoi(s)
  return K(asInt)
}

Mengapa ini baik-baik saja dan tidak membuat Anda menjadi sangat bingung dengan ambiguitas

Contoh Anda tidak baik-baik saja. Saya tidak peduli jika panggilan pertama membutuhkan argumen atau parameter tipe. Anda memiliki fungsi Print yang tidak mencetak apa pun. Dapatkah Anda membayangkan membaca/meninjau kode itu? Print("foo") dengan set kedua tanda kurung dihilangkan terlihat baik-baik saja tetapi diam-diam tidak ada operasi.

Jika Anda mengirimkan kode itu kepada saya di PR, saya akan meminta Anda untuk mengubah namanya menjadi PrintFunc atau MakePrintFunc atau PrintPlusFunc atau sesuatu yang mengomunikasikan perilakunya.

Saya menggunakan dan membaca sedikit kode D, dulu (~2008-2009), dan saya harus mengatakan ! selalu membuatku tersandung.

Hah, menarik. Saya tidak memiliki preferensi khusus untuk operator instantiasi; itu sepertinya pilihan yang layak.

Di Go, nama jenis dan nama variabel serta nama fungsi terlihat persis sama, namun orang jarang bingung tentang apa yang mereka lihat. Orang-orang memahami bahwa int64(v) adalah konversi tipe dan F(v) adalah pemanggilan fungsi, meskipun terlihat persis sama.

Saya setuju, orang biasanya dapat dengan cepat membedakan antara konversi jenis dan panggilan fungsi. Menurut Anda mengapa demikian?

Teori pribadi saya adalah bahwa tipe biasanya kata benda, dan fungsi biasanya kata kerja. Jadi ketika Anda melihat Noun(...) itu cukup jelas itu adalah konversi tipe, dan ketika Anda melihat Verb(...) itu adalah panggilan fungsi.

Kita perlu melihat kode nyata untuk melihat apakah argumen tipe benar-benar membingungkan dalam praktiknya. Jika ya, maka kita harus menyesuaikan sintaksnya. Dengan tidak adanya kode nyata, kita tidak tahu.

Itu masuk akal.

Secara pribadi, saya datang ke utas ini karena saya membaca draf Kontrak (mungkin 5 kali, setiap kali terpental dan kemudian semakin jauh ketika saya kembali lagi nanti) dan menemukan sintaks yang membingungkan dan asing. Saya menyukai konsepnya ketika saya akhirnya memahaminya, tetapi ada penghalang besar karena sintaks yang ambigu.

Ada banyak "kode nyata" di bagian bawah draf Kontrak, menangani semua kasus penggunaan umum itu, yang luar biasa! Namun, saya merasa sulit untuk menguraikan secara visual; Saya lebih lambat membaca dan memahami kodenya. Sepertinya saya harus melihat argumen dari berbagai hal dan konteks yang lebih luas untuk mengetahui apa itu dan apa aliran kontrolnya, dan sepertinya itu adalah langkah mundur dari kode biasa.

Mari kita ambil kode nyata ini:

import "container/orderedmap"

var m = orderedmap.New(string, string)(strings.Compare)

func Add(a, b string) {
    m.Insert(a, b)
}

Ketika saya membaca orderedmap.New( Saya mengharapkan apa yang berikut menjadi argumen untuk fungsi New , potongan-potongan informasi penting yang dibutuhkan peta yang dipesan untuk berfungsi. Tapi itu sebenarnya ada di set kedua tanda kurung. Saya terlempar oleh ini. Itu membuat kode lebih sulit untuk grok.

(Ini hanya satu contoh, tidak semua yang saya lihat itu ambigu, tetapi sulit untuk memiliki diskusi terperinci tentang serangkaian poin yang luas.)

Inilah yang saya sarankan:

// Instantiation operator
var m = orderedmap.New!(string, string)(strings.Compare)
// Alternate delimiters -- notice I don't insist on any particular kind
var m = orderedmap.New<|string, string|>(strings.Compare)
// Appropriately named function
var m = orderedmap.MakeConstructor(string, string)(strings.Compare)

Dalam dua contoh pertama, sintaks yang berbeda berfungsi untuk mematahkan asumsi saya bahwa kumpulan tanda kurung pertama berisi argumen untuk New() , sehingga kodenya tidak terlalu mengejutkan dan alurnya lebih dapat diamati dari tingkat tinggi.

Opsi ketiga menggunakan penamaan untuk membuat alurnya tidak mengejutkan. Saya sekarang berharap bahwa set tanda kurung pertama berisi argumen yang diperlukan untuk membuat fungsi konstruktor dan saya berharap bahwa nilai yang dikembalikan adalah fungsi konstruktor yang pada gilirannya dapat dipanggil untuk menghasilkan peta yang dipesan.


Saya pasti bisa membaca kode dalam gaya saat ini. Saya dapat membaca semua kode dalam draft Kontrak. Hanya saja lebih lambat karena butuh waktu lebih lama bagi saya untuk memprosesnya. Saya telah mencoba yang terbaik untuk menganalisis mengapa ini dan melaporkannya: selain contoh orderedmap.New , https://github.com/golang/go/issues/15292#issuecomment -623649521 memiliki ringkasan yang bagus , meskipun saya mungkin bisa menemukan lebih banyak lagi. Tingkat ambiguitas bervariasi antara contoh yang berbeda.

Saya mengakui bahwa saya tidak akan mendapatkan persetujuan semua orang, karena keterbacaan dan kejelasan agak subjektif dan mungkin dipengaruhi oleh latar belakang dan bahasa favorit orang tersebut. Saya pikir 4 jenis ambiguitas penguraian adalah indikator yang baik bahwa kami memiliki masalah.

import "container/orderedmap"

var m = orderedmap.NewOf(string, string)(strings.Compare)

func Add(a, b string) {
    m.Insert(a, b)
}

Saya pikir NewOf membaca lebih baik daripada New karena New biasanya mengembalikan sebuah instance, bukan generik yang membuat sebuah instance.


Anda memiliki fungsi Print yang tidak mencetak apa pun.

Untuk lebih jelasnya, karena ada beberapa inferensi tipe otomatis, Print(foo) generik akan menjadi panggilan cetak nyata melalui inferensi atau kesalahan. Di Go hari ini, pengenal kosong tidak diperbolehkan :

package main

import (
    "fmt"
)

func main() {
    fmt.Println
}

./prog.go:8:5: fmt.Println evaluated but not used

Saya bertanya-tanya apakah ada cara untuk membuat kesimpulan umum tidak terlalu membingungkan.

@toolbox

Contoh Anda tidak baik-baik saja. Saya tidak peduli jika panggilan pertama membutuhkan argumen atau parameter tipe. Anda memiliki fungsi Cetak yang tidak mencetak apa pun. Dapatkah Anda membayangkan membaca/meninjau kode itu?

Anda telah menghilangkan pertanyaan tindak lanjut yang relevan di sini. Saya setuju dengan Anda bahwa itu tidak benar-benar dapat dibaca. Tetapi Anda berdebat untuk penegakan batasan ini di tingkat bahasa. Saya tidak mengatakan "Anda baik-baik saja dengan ini" yang berarti "Anda baik-baik saja dengan kode ini", tetapi berarti "Anda baik-baik saja dengan bahasa yang mengizinkan kode itu".

Yang merupakan pertanyaan lanjutan saya. Apakah menurut Anda Go adalah bahasa yang lebih buruk, karena tidak memasukkan pembatasan nama untuk functions-that-return- func ? Jika tidak, mengapa akan menjadi bahasa yang lebih buruk jika kita tidak membatasi fungsi tersebut, ketika mereka mengambil argumen tipe alih-alih argumen nilai?

@Merovius

Tetapi Anda berdebat untuk penegakan batasan ini di tingkat bahasa.

Tidak, dia berpendapat bahwa mengandalkan standar penamaan adalah solusi potensial yang valid untuk masalah tersebut. Aturan informal seperti "penulis tipe didorong untuk memberi nama tipe generik mereka dengan cara yang tidak mudah dikacaukan dengan nama fungsi" adalah solusi yang valid untuk masalah ambiguitas, karena di dalamnya akan benar-benar menyelesaikan masalah dalam kasus individu.

Dia tidak mengisyaratkan di mana pun bahwa solusi ini harus ditegakkan oleh bahasa, dia mengatakan bahwa jika pengelola memutuskan untuk mempertahankan proposal saat ini apa adanya, bahkan ada solusi praktis potensial untuk masalah ambiguitas. Dan dia mengklaim bahwa masalah ambiguitas itu nyata dan penting untuk dipertimbangkan.

Sunting: Saya pikir kami sedikit menyimpang dari jalur. Saya pikir lebih banyak kode contoh "nyata" akan sangat bermanfaat untuk percakapan pada saat ini.

Tidak, dia berpendapat bahwa mengandalkan standar penamaan adalah solusi potensial yang valid untuk masalah tersebut.

Apakah mereka? Saya mencoba bertanya secara khusus:

Perhatikan bahwa yang pertama setidaknya 100% kompatibel dengan desain. Itu tidak meresepkan bentuk apa pun untuk pengidentifikasi yang digunakan dan saya harap Anda tidak menyarankan untuk meresepkannya (dan jika Anda melakukannya, saya akan tertarik mengapa aturan yang sama tidak berlaku hanya untuk mengembalikan fungsi).

Anda benar, saya menyarankan itu :)

Saya setuju bahwa "meresepkan" tidak terlalu spesifik di sini, tetapi setidaknya itulah pertanyaan yang saya maksudkan. Jika mereka memang tidak berdebat demi persyaratan tingkat bahasa yang ada di dalam desain, saya mohon maaf atas kesalahpahaman, tentu saja. Tapi saya merasa dibenarkan dengan asumsi bahwa "resep" setidaknya lebih kuat dari "aturan informal", setidaknya. Terutama jika dimasukkan ke dalam konteks dua saran lain yang mereka ajukan (dengan pijakan yang sama) yang merupakan konstruksi tingkat bahasa karena mereka bahkan tidak menggunakan pengidentifikasi yang valid saat ini.

Apakah akan ada rencana serupa vgo yang memungkinkan komunitas mencoba proposal generik terbaru?

Setelah bermain sedikit dengan taman bermain yang mendukung kontrak, saya tidak benar-benar melihat apa yang diributkan tentang perlunya membedakan antara argumen tipe dan yang biasa.

Pertimbangkan contoh ini . Saya meninggalkan inisialisasi tipe pada semua fungsi, meskipun saya bisa menghilangkan semuanya dan itu masih akan dikompilasi dengan baik. Ini tampaknya menunjukkan bahwa sebagian besar kode potensial semacam itu bahkan tidak akan menyertakannya, yang pada gilirannya tidak akan menyebabkan kebingungan.

Namun, jika parameter tipe ini disertakan, pengamatan tertentu dapat dilakukan:
a) tipenya adalah tipe bawaan, yang diketahui semua orang dan dapat segera diidentifikasi
b) jenisnya adalah pihak ke-3, dan dalam hal ini adalah TitleCased, yang akan membuatnya sedikit menonjol. Ya, itu mungkin, meskipun tidak mungkin, bahwa itu bisa menjadi fungsi yang mengembalikan fungsi lain, dan panggilan pertama menggunakan variabel yang diekspor pihak ke-3, tetapi saya pikir ini sangat jarang.
c) jenisnya adalah beberapa jenis pribadi. Dalam hal ini, mereka akan terlihat lebih seperti pengidentifikasi variabel biasa. Namun, karena tidak diekspor, ini berarti bahwa kode yang dilihat pembaca bukanlah bagian dari beberapa dokumentasi yang mereka coba uraikan, dan, yang lebih penting, mereka sudah membaca kodenya. Oleh karena itu mereka dapat melakukan langkah ekstra dan langsung beralih ke definisi fungsi untuk menghilangkan ambiguitas.

Keributannya adalah tentang tampilannya tanpa obat generik https://play.golang.org/p/7BRdM2S5dwQ dan untuk seseorang yang baru memprogram Stack terpisah untuk setiap jenis seperti StackString, StackInt, ... lebih mudah untuk diprogram kemudian Stack(T) dalam proposal sintaks generik saat ini. Saya tidak ragu proposal saat ini dipikirkan dengan baik seperti yang ditunjukkan oleh contoh Anda tetapi nilai kesederhanaan dan kejelasan dikurangi dengan membagikan. Saya mengerti prioritas pertama adalah untuk mengetahui apakah itu bekerja dengan pengujian, tetapi begitu kami setuju proposal saat ini mencakup sebagian besar kasus dan tidak ada kesulitan kompiler teknis, prioritas yang lebih tinggi adalah membuatnya dapat dimengerti oleh semua orang yang selalu menjadi alasan nomor satu Sukses dari awal.

@Merovius Tidak, seperti yang @infogulch katakan, maksud saya membuat konvensi ala -er pada antarmuka. Saya sebutkan itu di atas, maaf atas kebingungannya. (Saya seorang "dia" btw.)

Pertimbangkan contoh ini. Saya meninggalkan inisialisasi tipe pada semua fungsi, meskipun saya bisa menghilangkan semuanya dan itu masih akan dikompilasi dengan baik. Ini tampaknya menunjukkan bahwa sebagian besar kode potensial semacam itu bahkan tidak akan menyertakannya, yang pada gilirannya tidak akan menyebabkan kebingungan.

Bagaimana dengan contoh yang sama dalam versi bercabang dari taman bermain generik?

Saya menggunakan ::<> untuk klausa parameter tipe, dan jika ada satu tipe, Anda dapat menghilangkan <> . Seharusnya tidak ada ambiguitas parser pada kurung siku, dan ini memudahkan saya untuk membaca kode—baik kode generik maupun kode menggunakan generik. (Dan jika tipe params disimpulkan, itu lebih baik.)

Seperti yang saya katakan sebelumnya, saya tidak terjebak pada ! untuk instantiasi tipe (dan saya pikir :: terlihat lebih baik setelah ditinjau). Dan itu hanya membantu di mana obat generik digunakan, tidak begitu banyak dalam deklarasi. Jadi ini agak menggabungkan keduanya, menghilangkan <> jika tidak perlu, agak seperti menghilangkan menyertakan () untuk parameter pengembalian fungsi jika hanya ada satu.

Contoh kutipan:

type Stack::<type E> []E

func (s Stack::E) Peek() E {
    return s[len(s)-1]
}

func (s *Stack::E) Pop() {
    *s = (*s)[:len(*s)-1]
}

func (s *Stack::E) Push(value E) {
    *s = append(*s, value)
}

type StackIterator::<type E> struct{
    stack Stack::E
    current int
}

func (s *Stack::E) Iter() Iterator::E {
    it := StackIterator::E{stack: *s, current: len(*s)}

    return &it
}

func (i *StackIterator::E) Next() (bool) { 
    i.current--

    if i.current < 0 { 
        return false
    }

    return true
}

func (i *StackIterator::E) Value() E { 
    if i.current < 0 {
        var zero E
        return zero
    }

    return i.stack[i.current]
}

// ...

var it Iterator::string = stack.Iter()

it = Filter::string(it, func(s string) bool {
    return s == "foo" || s == "beta" || s == "delta"
})

it = Map::<string, string>(it, func(s string) string {
    return s + ":1"
})

it = Distinct::string(it)

println(Reduce(it, "", func(a, b string) string {
    if a == "" {
        return b
    }
        return a + ":" + b
}))

Untuk contoh ini, saya juga menyesuaikan nama variabel, saya pikir E untuk "Elemen" lebih mudah dibaca daripada T untuk "Jenis".

Seperti yang saya katakan, dengan membuat hal-hal generik terlihat berbeda, kode Go yang mendasarinya menjadi terlihat. Anda tahu apa yang Anda lihat, aliran kontrolnya jelas, tidak ada ambiguitas, dll.

Ini juga baik-baik saja dengan lebih banyak inferensi tipe:

var it Iterator::string = stack.Iter()

it = Filter(it, func(s string) bool {
    return s == "foo" || s == "beta" || s == "delta"
})

it = Map::<string, string>(it, func(s string) string {
    return s + ":1"
})

it = Distinct(it)

println(Reduce(it, "", func(a, b string) string {
    if a == "" {
        return b
    }
        return a + ":" + b
}))

@toolbox Mohon maaf, kalau begitu, kami berbicara melewati satu sama lain :)

seseorang yang baru memprogram Stack terpisah untuk setiap jenis seperti StackString, StackInt, ... lebih mudah diprogram daripada Stack(T)

Saya benar-benar akan terkejut jika itu yang terjadi. Tidak ada seorang pun yang sempurna, dan bug pertama yang menyelinap sendiri bahkan ke dalam sepotong kode sederhana akan menunjukkan betapa salahnya pernyataan itu dalam jangka panjang.

Inti dari contoh saya adalah untuk mengilustrasikan penggunaan fungsi parametrik dan instantiasinya dengan tipe konkret, yang merupakan inti dari diskusi ini, bukan apakah implementasi sampel Stack bagus atau tidak.

Inti dari contoh saya adalah untuk menggambarkan penggunaan fungsi parametrik dan instantiasinya dengan tipe konkret, yang merupakan inti dari diskusi ini, bukan apakah implementasi Stack sampel bagus atau tidak.

Saya tidak berpikir @gertcuykens dimaksudkan untuk mengetuk implementasi Stack Anda, sepertinya dia merasa bahwa sintaks generik tidak dikenal dan sulit dipahami.

Namun, jika parameter tipe ini disertakan, pengamatan tertentu dapat dilakukan:
(a)...(b)...(c)...(d)...

Saya melihat semua poin Anda, menghargai analisis Anda, dan itu tidak salah. Anda benar bahwa, dalam sebagian besar kasus, dengan memeriksa kode dengan cermat, Anda dapat menentukan apa yang dilakukannya. Saya tidak berpikir itu menyangkal laporan pengembang Go yang mengatakan sintaksnya membingungkan, ambigu, atau membutuhkan waktu lebih lama untuk dibaca, bahkan jika mereka akhirnya bisa membacanya.

Secara umum, sintaksnya berada di lembah yang luar biasa. Kode melakukan sesuatu yang berbeda, tetapi terlihat cukup mirip dengan konstruksi yang ada sehingga harapan Anda dilemparkan dan daya pandang turun. Anda juga tidak dapat menetapkan harapan baru karena (secara tepat) elemen-elemen ini opsional, baik secara keseluruhan maupun sebagian.

Untuk kasus patologis yang lebih spesifik, @infogulch menyatakannya dengan baik:

Saya tidak berpikir bahwa memasukkan semua kompleksitas kognitif ke sudut-sudut gelap bahasa hanya karena mereka jarang digunakan juga merupakan ide yang bagus. Bahkan jika itu cukup langka sehingga kebanyakan orang biasanya tidak menemukannya, mereka kadang-kadang masih akan menemukannya, dan membiarkan beberapa kode memiliki aliran kontrol yang membingungkan selama itu bukan kode "kebanyakan" meninggalkan rasa tidak enak di mulut saya.

Saya pikir, pada titik ini, kita mencapai kejenuhan artikulasi pada bagian topik tertentu. Tidak peduli seberapa banyak kita membicarakannya, ujian asamnya adalah seberapa cepat dan seberapa baik pengembang Go dapat mempelajarinya, membacanya, dan menulisnya.

(Dan ya, sebelum disebutkan, beban harus ada pada penulis perpustakaan, bukan pengembang klien, tapi saya rasa kita tidak menginginkan Efek Peningkatan di mana perpustakaan umum tidak dapat dipahami oleh orang di jalan. Saya juga tidak' Saya tidak ingin Go berubah menjadi Jambore Umum, tetapi sebagian saya percaya bahwa kelalaian desain akan membatasi penyebarannya .)

Kami memiliki taman bermain dan kami dapat membuat garpu untuk sintaks lain , yang fantastis. Mungkin kita membutuhkan lebih banyak alat!

Orang-orang telah memberikan umpan balik . Saya yakin lebih banyak umpan balik diperlukan, dan mungkin kita membutuhkan sistem umpan balik yang lebih baik atau lebih efisien.

@toolbox Menurut Anda apakah mungkin untuk mengurai kode ketika Anda selalu menghilangkan <> dan type seperti itu? Mungkin memerlukan proposal yang lebih ketat tentang apa yang bisa dilakukan, tapi mungkin itu sepadan dengan pertukarannya?

type Stack::E []E

func (s Stack::E) Peek() E {
    return s[len(s)-1]
}

func (s *Stack::E) Pop() {
    *s = (*s)[:len(*s)-1]
}

func (s *Stack::E) Push(value E) {
    *s = append(*s, value)
}

type StackIterator::E struct{
    stack Stack::E
    current int
}

func (s *Stack::E) Iter() Iterator::E {
    it := StackIterator::E{stack: *s, current: len(*s)}

    return &it
}

func (i *StackIterator::E) Next() (bool) { 
    i.current--

    if i.current < 0 { 
        return false
    }

    return true
}

func (i *StackIterator::E) Value() E { 
    if i.current < 0 {
        var zero E
        return zero
    }

    return i.stack[i.current]
}

// ...

var it Iterator::string = stack.Iter()

it = Filter::string(it, func(s string) bool {
    return s == "foo" || s == "beta" || s == "delta"
})

it = Map::string, string (it, func(s string) string {
    return s + ":1"
})

it = Distinct::string(it)

println(Reduce(it, "", func(a, b string) string {
    if a == "" {
        return b
    }
        return a + ":" + b
}))

Saya tidak tahu kenapa, tapi Map::string, string (... ini terasa aneh saja. Sepertinya ini membuat 2 token, Map::string , dan panggilan fungsi string .

Juga, meskipun ini tidak digunakan di Go, menggunakan "Identifier :: Identifier" mungkin memberikan kesan yang salah kepada pengguna pertama kali, berpikir bahwa ada Filter class/namespace dengan string fungsi di dalamnya. Menggunakan kembali token dari bahasa lain yang diadopsi secara luas untuk sesuatu yang sama sekali berbeda akan menyebabkan banyak kebingungan.

Apakah menurut Anda mungkin untuk mengurai kode ketika Anda selalu menghilangkan <> dan mengetik seperti itu? Mungkin memerlukan proposal yang lebih ketat tentang apa yang bisa dilakukan, tapi mungkin itu sepadan dengan pertukarannya?

Tidak, saya rasa tidak. Saya setuju dengan @urandom bahwa karakter spasi, tanpa penutup apa pun, membuatnya tampak seperti dua token. Saya pribadi juga menyukai ruang lingkup Kontrak dan tidak tertarik untuk mengubah kemampuannya.

Juga, meskipun ini tidak digunakan di Go, menggunakan "Identifier::Identifier" mungkin memberikan kesan yang salah kepada pengguna pertama kali, berpikir bahwa ada kelas Filter/namespace dengan fungsi string di dalamnya. Menggunakan kembali token dari bahasa lain yang diadopsi secara luas untuk sesuatu yang sama sekali berbeda akan menyebabkan banyak kebingungan.

Saya sebenarnya belum pernah menggunakan bahasa dengan :: tetapi saya telah melihatnya. Mungkin ! lebih baik karena itu akan cocok dengan D, meskipun saya menemukan :: terlihat lebih baik secara visual.

Jika kita menempuh jalan ini, akan ada banyak diskusi tentang karakter apa yang akan digunakan secara spesifik. Berikut adalah upaya untuk mempersempit apa yang kami cari:

  • Sesuatu selain identifier() telanjang sehingga tidak terlihat seperti panggilan fungsi.
  • Sesuatu yang dapat menyertakan beberapa parameter tipe, untuk menyatukannya secara visual dengan cara yang dapat dilakukan oleh tanda kurung.
  • Sesuatu yang terlihat terhubung dengan pengenal sehingga terlihat seperti satu kesatuan.
  • Sesuatu yang tidak ambigu untuk parser.
  • Sesuatu yang tidak bertentangan dengan konsep lain yang memiliki mindshare pengembang yang kuat.
  • Jika memungkinkan, sesuatu yang akan mempengaruhi definisi serta penggunaan obat generik, sehingga menjadi lebih mudah dibaca juga.

Ada banyak hal yang bisa muat.

  • identifier!(a, b) ( taman bermain )
  • identifier@(a, b)
  • identifier#(a, b)
  • identifier$(a, b)
  • identifier<:a, b:>
  • identifier.<a, b> itu seperti pernyataan tipe!
  • identifier:<a, b>
  • dll.

Adakah yang punya ide tentang cara mempersempit kumpulan potensi lebih lanjut?

Hanya catatan singkat bahwa kami telah mempertimbangkan semua ide itu, dan kami juga mempertimbangkan ide seperti

func F(T : a, b T) { }
func G() { F(int : 1, 2) }

Tapi sekali lagi, buktinya puding ada pada saat disantap. Diskusi abstrak tanpa adanya kode layak dilakukan tetapi tidak mengarah pada kesimpulan yang pasti.

(Tidak yakin apakah ini telah dibicarakan sebelumnya) Saya melihat bahwa dalam kasus di mana kami menerima struct, kami tidak akan dapat "memperluas" API yang ada untuk menangani tipe generik tanpa merusak kode panggilan yang ada.

Misalnya, mengingat fungsi non-generik ini

func Repeat(v, n int) []int {
    var r []int
    for i := n; i > 0; i-- {
        r = append(r, v)
    }
    return r
}

Repeat(4, 4)

Kami dapat membuatnya generik tanpa merusak kompatibilitas ke belakang

func Repeat(type T)(v T, n int) []T {
    var r []T
    for i := n; i > 0; i-- {
        r = append(r, v)
    }
    return r
}

Repeat("a", 5)

Tetapi jika kita ingin melakukan hal yang sama dengan fungsi yang menerima struct generik

type XY struct {
    X, Y int
}

func RangeRepeat(arr []XY) []int {
    var r []int
    for _, n := range arr {
        for i := n.Y; i > 0; i-- {
            r = append(r, n.X)
        }
    }
    return r
}

RangeRepeat([]XY{{1, 1}, {2, 2}, {3, 3}})

sepertinya kode panggilan perlu diperbarui

type XY(type T) struct {
    X T
    Y int
}

func RangeRepeat(type T)(arr []XY(T)) []T {
    var r []T
    for _, n := range arr {
        for i := n.Y; i > 0; i-- {
            r = append(r, n.X)
        }
    }
    return r
}

// error: cannot use generic type XY(type T any) without instantiation
// RangeRepeat([]XY{{1, 1}, {2, 2}, {3, 3}}) // error in old code
RangeRepeat([](XY(int)){{1, 1}, {2, 2}, {3, 3}}) // API changed
// RangeRepeat([]XY{{"1", 1}, {"2", 2}, {"3", 3}}) // error
RangeRepeat([](XY(string)){{"1", 1}, {"2", 2}, {"3", 3}}) // ok

Akan luar biasa untuk dapat menurunkan tipe dari struktur juga.

@ianlancetaylor

Draf kontrak menyebutkan bahwa methods may not take additional type arguments . Namun, tidak disebutkan penggantian kontrak untuk metode tertentu. Fitur seperti itu akan sangat berguna untuk mengimplementasikan antarmuka tergantung pada kontrak apa yang terikat dengan tipe parametrik.

Sudahkah Anda membahas kemungkinan seperti itu?

Pertanyaan lain untuk draft kontrak. Akankah disjungsi tipe dibatasi untuk tipe bawaan? Jika tidak, apakah mungkin menggunakan tipe parametrized, terutama antarmuka dalam daftar disjungsi?

Sesuatu seperti

type Getter(T) interface {
    Get() T
}

contract(G, T) {
    G Getter(T)
}

akan sangat berguna, tidak hanya untuk menghindari duplikasi metode yang ditetapkan dari antarmuka ke kontrak, tetapi juga untuk membuat instance tipe parametrized ketika inferensi tipe gagal, dan Anda tidak memiliki akses ke tipe konkret (mis.

@ianlancetaylor Saya tidak yakin apakah ini telah dibahas sebelumnya, tetapi mengenai sintaks untuk argumen tipe ke suatu fungsi, apakah mungkin untuk menggabungkan daftar argumen ke daftar argumen tipe? Jadi untuk contoh grafik, alih-alih

var g = graph.New(*Vertex, *FromTo)([]*Vertex{ ... })

Anda akan menggunakan

var g = graph.New(*Vertex, *FromTo, []*Vertex{ ... })

Pada dasarnya, argumen K pertama dari daftar argumen sesuai dengan daftar argumen tipe dengan panjang K. Sisa dari daftar argumen sesuai dengan argumen reguler ke fungsi. Ini memiliki manfaat mencerminkan sintaks dari

make(Type, size)

yang mengambil Tipe sebagai argumen pertama.

Ini akan menyederhanakan tata bahasa, tetapi membutuhkan informasi tipe untuk mengetahui di mana argumen tipe berakhir, dan argumen reguler dimulai.

@ smasher164 Dia mengatakan beberapa komentar kembali bahwa mereka mempertimbangkannya (yang berarti mereka membuangnya, meskipun saya ingin tahu mengapa).

func F(T : a, b T) { }
func G() { F(int : 1, 2) }

Itulah yang Anda sarankan, tetapi dengan titik dua untuk memisahkan dua jenis argumen. Secara pribadi saya cukup menyukainya, meskipun gambarnya tidak lengkap; bagaimana dengan deklarasi tipe, metode, instantiasi, dll.

Saya ingin kembali ke sesuatu yang @Inuart katakan:

Kami dapat membuatnya generik tanpa merusak kompatibilitas ke belakang

Apakah tim Go akan mempertimbangkan untuk mengubah pustaka standar dengan cara ini agar konsisten dengan jaminan kompatibilitas Go 1? Misalnya, bagaimana jika strings.Repeat(s string, count int) string diganti dengan Repeat(type S stringlike)(s S, count int) S ? Anda juga dapat menambahkan komentar //Deprecated ke bytes.Repeat tetapi biarkan di sana agar kode lama dapat digunakan. Apakah itu sesuatu yang akan dipertimbangkan oleh tim Go?

Sunting: untuk lebih jelasnya, maksud saya, apakah ini akan dipertimbangkan dalam Go1Compat secara umum? Abaikan contoh spesifik jika Anda tidak menyukainya.

@carlmjohnson Tidak. Kode ini akan rusak: f := strings.Repeat , karena fungsi polimorfik tidak dapat direferensikan tanpa membuat instance terlebih dahulu.

Dan dari sana, saya pikir penggabungan argumen tipe dan argumen nilai akan menjadi kesalahan, karena mencegah sintaks alami untuk merujuk ke versi fungsi yang dipakai. Akan lebih alami jika go sudah memiliki kari, tetapi tidak. Tampaknya aneh memiliki ekspresi foo(int, 42) dan foo(int) dan keduanya memiliki tipe yang sangat berbeda.

@urandom Ya, kami telah membahas kemungkinan menambahkan batasan tambahan pada parameter tipe dari metode individual. Itu akan menyebabkan kumpulan metode dari tipe berparameter bervariasi berdasarkan argumen tipe. Ini mungkin berguna, atau mungkin membingungkan, tetapi satu hal yang tampaknya pasti: kita dapat menambahkannya nanti tanpa merusak apa pun. Jadi kami menunda ide itu untuk nanti. Terima kasih telah mengangkatnya.

Persisnya apa yang dapat dicantumkan dalam daftar jenis yang diizinkan tidak sejelas mungkin. Saya pikir kami memiliki lebih banyak pekerjaan yang harus dilakukan di sana. Perhatikan bahwa setidaknya dalam draf desain saat ini yang mencantumkan tipe antarmuka dalam daftar tipe saat ini berarti argumen tipe dapat berupa tipe antarmuka tersebut. Ini tidak berarti bahwa argumen tipe bisa menjadi tipe yang mengimplementasikan tipe antarmuka itu. Saya pikir saat ini tidak jelas apakah itu bisa menjadi instance instantiated dari tipe parameter. Ini pertanyaan yang bagus.

@smasher164 @toolbox Kasus yang perlu dipertimbangkan ketika melihat menggabungkan parameter tipe dan parameter reguler dalam satu daftar adalah bagaimana memisahkannya (jika dipisahkan) dan bagaimana menangani kasus di mana tidak ada parameter reguler (mungkin kita dapat mengecualikan kasus tidak ada parameter tipe). Misalnya, jika tidak ada parameter reguler, bagaimana Anda membedakan antara membuat instance fungsi tetapi tidak memanggilnya, dan membuat instance fungsi dan memanggilnya? Meskipun jelas yang terakhir adalah kasus yang lebih umum, masuk akal jika orang ingin dapat menulis kasus sebelumnya.

Jika parameter tipe ditempatkan di dalam tanda kurung yang sama dengan parameter biasa, maka @griesemer mengatakan di #36177 (postingan keduanya) bahwa ia cukup menyukai penggunaan titik koma daripada titik dua sebagai pemisah karena (sebagai hasilnya penyisipan titik koma otomatis) memungkinkan seseorang untuk menyebarkan parameter ke beberapa baris dengan cara yang bagus.

Secara pribadi, saya juga menyukai penggunaan batang vertikal ( |..| ) untuk menyertakan parameter tipe karena terkadang Anda melihatnya digunakan dalam bahasa lain (Ruby, Crystal, dll.) untuk menyertakan blok parameter. Jadi kita akan memiliki hal-hal seperti:

func F(|T| a, b T) { }
func G() { F(|int| 1, 2) }

Keuntungan meliputi:

  • Mereka memberikan perbedaan visual yang bagus (setidaknya di mata saya) antara tipe dan parameter reguler.
  • Anda tidak perlu menggunakan kata kunci type .
  • Tidak memiliki parameter reguler tidak menjadi masalah.
  • Karakter bilah vertikal, tentu saja, ada dalam set ASCII dan karenanya harus tersedia di sebagian besar keyboard.

Anda bahkan mungkin dapat menggunakannya di luar tanda kurung tetapi mungkin Anda akan mengalami kesulitan penguraian yang sama dengan <...> atau [...] karena dapat disalahartikan sebagai operator bitwise 'atau' meskipun mungkin kesulitan akan kurang akut.

Saya tidak mengerti bagaimana bilah vertikal membantu jika tidak ada parameter reguler. Saya tidak mengerti bagaimana Anda dapat membedakan instantiasi fungsi dari panggilan fungsi.

Salah satu cara untuk membedakan antara kedua kasus tersebut adalah dengan meminta kata kunci type jika Anda membuat instance fungsi tetapi tidak jika Anda memanggilnya yang, seperti yang Anda katakan sebelumnya, adalah kasus yang lebih umum.

Saya setuju bahwa itu bisa berhasil, tetapi tampaknya sangat halus. Saya tidak berpikir itu akan menjadi jelas bagi pembaca apa yang terjadi.

Saya pikir di Go kita perlu membidik lebih tinggi daripada sekadar memiliki cara untuk melakukan sesuatu. Kita perlu membidik pendekatan yang lugas, intuitif, dan cocok dengan bahasa lainnya. Orang yang membaca kode harus dapat dengan mudah memahami apa yang terjadi. Tentu saja kita tidak selalu bisa memenuhi tujuan itu, tetapi kita harus melakukan yang terbaik yang kita bisa.

@ianlancetaylor selain berdebat tentang sintaks, yang menarik dengan sendirinya, saya ingin tahu apakah ada yang bisa kami lakukan sebagai komunitas untuk membantu Anda & tim dalam hal ini.

Misalnya, saya mendapatkan ide bahwa Anda ingin lebih banyak kode ditulis dalam gaya proposal, sehingga dapat mengevaluasi proposal dengan lebih baik, baik secara sintaksis maupun sebaliknya? Dan/atau hal lain?

@toolbox Ya. Kami sedang mengerjakan alat untuk membuatnya lebih mudah, tetapi belum siap. Nyata Segera Sekarang.

Bisakah Anda mengatakan lebih banyak tentang alat ini? Apakah itu memungkinkan mengeksekusi kode?

Apakah masalah ini merupakan lokasi yang disukai untuk umpan balik obat generik? Tampaknya lebih aktif daripada wiki. Satu pengamatan adalah ada banyak aspek pada proposal, tetapi masalah GitHub menciutkan diskusi ke dalam format linier.

Sintaks F(T:) / G() { F(T:)} terlihat baik bagi saya. Saya tidak berpikir instantiasi yang terlihat seperti panggilan fungsi akan intuitif untuk pembaca yang tidak berpengalaman.

Saya tidak mengerti persis apa yang menjadi perhatian di sekitar kompatibilitas ke belakang. Saya pikir ada batasan dalam rancangan untuk menyatakan kontrak kecuali di tingkat atas. Mungkin ada baiknya menimbang (dan mengukur) berapa banyak kode yang benar-benar akan rusak jika ini diizinkan. Pemahaman saya hanya kode yang menggunakan kata kunci contract , yang sepertinya tidak banyak kode (yang bagaimanapun dapat didukung dengan menentukan go1 di bagian atas file lama). Timbang itu dengan puluhan tahun lebih banyak kekuatan untuk programmer. Secara umum tampaknya cukup sederhana untuk melindungi kode lama dengan mekanisme seperti itu terutama dengan meluasnya penggunaan alat-alat go yang terkenal.

Lebih lanjut mengenai pembatasan itu, saya menduga larangan mendeklarasikan metode dalam badan fungsi adalah alasan mengapa antarmuka tidak digunakan lebih banyak - mereka jauh lebih rumit daripada melewatkan fungsi tunggal. Sulit untuk mengatakan apakah pembatasan tingkat atas kontrak akan sama menjengkelkannya dengan pembatasan metode--mungkin tidak akan--tapi tolong jangan gunakan pembatasan metode sebagai preseden. Bagi saya itu adalah kesalahan bahasa.

Saya juga ingin melihat contoh bagaimana kontrak dapat membantu mengurangi verbositas if err != nil , dan yang lebih penting jika kontrak tidak mencukupi. Apakah sesuatu seperti F() (X, error) {return IfError(foo(), func(i, j int) X { return X(i*j}), Identity )} mungkin?

Saya juga bertanya-tanya apakah tim go mengantisipasi bahwa tanda tangan fungsi implisit akan terasa seperti fitur yang hilang begitu Peta, Filter, dan teman tersedia. Apakah ini sesuatu yang perlu dipertimbangkan saat fitur pengetikan implisit baru ditambahkan ke bahasa untuk kontrak? Atau bisa ditambahkan nanti? Atau tidak akan pernah menjadi bagian dari bahasa?

Berharap untuk mencoba proposal. Maaf untuk banyak topik.

Secara pribadi saya cukup skeptis bahwa banyak orang ingin menulis metode dalam badan fungsi. Sangat jarang untuk mendefinisikan tipe dalam badan fungsi saat ini; mendeklarasikan metode akan lebih jarang lagi. Yang mengatakan, lihat #25860 (tidak terkait dengan obat generik).

Saya tidak melihat bagaimana obat generik membantu penanganan kesalahan (sudah menjadi topik yang sangat bertele-tele). Saya tidak mengerti contoh Anda, maaf.

Sintaks literal fungsi yang lebih pendek, juga tidak terhubung ke generik, adalah #21498.

Ketika saya memposting tadi malam, saya tidak menyadari bahwa mungkin untuk bermain dengan draft
penerapan (!!). Wow, senang akhirnya bisa menulis kode yang lebih abstrak. Saya tidak memiliki masalah dengan sintaks draf.

Melanjutkan pembahasan di atas...


Sebagian alasan orang tidak menulis tipe di badan fungsi adalah karena mereka
tidak dapat menulis metode untuk mereka. Pembatasan ini dapat menjebak tipe di dalam
blok di mana itu didefinisikan, karena tidak dapat diubah secara ringkas menjadi
antarmuka untuk digunakan di tempat lain. Java memungkinkan kelas anonim untuk memenuhi versinya
antarmuka, dan mereka digunakan cukup banyak.

Kita dapat memiliki diskusi antarmuka di #25860. Saya hanya akan mengatakan itu di era
kontrak, metode akan menjadi lebih penting, jadi saya sarankan untuk berbuat salah pada
sisi pemberdayaan tipe & orang lokal yang suka menulis penutup, bukan
melemahkan mereka.

(Dan untuk mengulangi, tolong jangan gunakan kompatibilitas go1 yang ketat [vs virtual
Kompatibilitas 99,999%, seperti yang saya pahami] sebagai faktor dalam memutuskan tentang ini
fitur.)


Mengenai penanganan kesalahan, saya menduga obat generik memungkinkan abstraksi
pola umum untuk menangani (T1, T2, ..., error) mengembalikan tupel. Bukan saya
memiliki sesuatu yang rinci dalam pikiran. Sesuatu seperti type ErrPair(type T) struct{T T; Err Error} mungkin berguna untuk menyatukan tindakan, seperti Promise di
Java / TypeScript. Mungkin seseorang telah memikirkan ini lebih jauh. Sebuah upaya
menulis perpustakaan pembantu dan kode yang menggunakan perpustakaan mungkin layak untuk dilihat
di jika Anda sedang mencari penggunaan nyata.

Dengan beberapa eksperimen saya berakhir dengan yang berikut ini. Saya ingin mencoba ini
teknik pada contoh yang lebih besar untuk melihat apakah menggunakan ErrPair(T) benar-benar membantu.

type result struct {min, max point}

// with a generic ErrPair type and generic function errMap2 (like Java's Optional#map() function).
func minMax2(msg *inputTimeSeries) (result, error) {
    return errMap2(
        MakeErrPair(time.Parse(layout, msg.start)).withMessage("bad start"),
        MakeErrPair(time.Parse(layout, msg.end)).withMessage("bad end"),
        func(start, end time.Time) (result, error) {
            min, max := argminmax(msg.inputPoints, func(p inputPoint) float64 {
                return float64(p.value)
            })
            mkPoint := func(ip inputPoint) point {
                return point{interpTime(start, end, ip.interp).Format(layout), ip.value}
            }
            return result{mkPoint(*min), mkPoint(*max)}, nil
        }).tuple()
}

// without generics, lots of if err != nil 
func minMax(msg *inputTimeSeries) (result, error) { 
    start, err := time.Parse(layout, msg.start)
    if err != nil {
        return result{}, fmt.Errorf("bad start: %w", err)
    }
    end, err := time.Parse(layout, msg.end)
    if err != nil {
        return result{}, fmt.Errorf("bad end: %w", err)
    }
    min, max := argminmax(msg.inputPoints, func(p inputPoint) float64 {
        return float64(p.value)
    })
    mkPoint := func(ip inputPoint) point {
        return point{interpTime(start, end, ip.interp).Format(layout), ip.value}
    }
    return result{mkPoint(*min), mkPoint(*max)}, nil
}

// Most languages look more like this.
func minMaxWithThrowing(msg *inputTimeSeries) result {
    start := time.Parse(layout, msg.start)) // might throw
    end := time.Parse(layout, msg.end)) // might throw
    min, max := argminmax(msg.inputPoints, func(p inputPoint) float64 {
        return float64(p.value)
    })
    mkPoint := func(ip inputPoint) point {
        return point{interpTime(start, end, ip.interp).Format(layout), ip.value}
    }
    return result{mkPoint(*min), mkPoint(*max)}
}

(kode contoh lengkap tersedia di sini )


Untuk eksperimen umum, saya mencoba menulis paket S-Expression
di sini .
Saya mengalami beberapa kepanikan dalam implementasi eksperimental ketika mencoba untuk
bekerja dengan tipe gabungan seperti Form([]*Form(T)) . Saya dapat memberikan lebih banyak umpan balik
setelah bekerja di sekitar itu, jika itu akan berguna.

Saya juga tidak yakin bagaimana menulis tipe primitif -> fungsi string:

contract PrimitiveType(T) {
    T bool, int, int8, int16, int32, int64, string, uint, uint8, uint16, uint32, uint64, float32, float64, complex64, complex128
    // string(T) is not a contract
}

func primitiveString(type T PrimitiveType(T))(t T) string  {
    // I'm not sure if this is an artifact of the experimental implementation or not.
    return string(t) // error: `cannot convert t (variable of type T) to string`
}

Fungsi sebenarnya yang saya coba tulis adalah yang ini:

// basicFormAdapter implements FormAdapter() for the primitive types.
type basicFormAdapter(type T PrimitiveType) struct{}


func (a *basicFormAdapter(T)) Format(e T, fc *FormatContext) error {
    //This doesn't work: fc.Print(string(e)) -- cannot convert e (variable of type T) to string
    // This also doesn't work: cannot type switch on non-interface value e (type int)
    // switch ee := e.(type) {
    // case int: fc.Print(string(ee))
    // default: fc.Print(fmt.Sprintf("!!! unsupported type %v", e))
    // }
    // IMO, the proposal to allow switching on T is most natural:
    // switch T.(type) {
    //  case int: fc.Print(string(e))
    //  default: fc.Print(fmt.Sprintf("!!! unsupported type %v", e))
    // }

    // This can't be the only way, right?
    rv := reflect.ValueOf(e)
    switch rv.Kind() {
    case reflect.Bool: fc.Print(fmt.Sprintf("%v", e))
    case reflect.Int:fc.Print(fmt.Sprintf("%v", e))
    case reflect.Int8: fc.Print(fmt.Sprintf("int8:%v", e))
    case reflect.Int16: fc.Print(fmt.Sprintf("int16:%v", e))
    case reflect.Int32: fc.Print(fmt.Sprintf("int32:%v", e))
    case reflect.Int64: fc.Print(fmt.Sprintf("int64:%v", e))
    case reflect.Uint: fc.Print(fmt.Sprintf("uint:%v", e))
    case reflect.Uint8: fc.Print(fmt.Sprintf("uint8:%v", e))
    case reflect.Uint16: fc.Print(fmt.Sprintf("uint16:%v", e))
    case reflect.Uint32: fc.Print(fmt.Sprintf("uint32:%v", e))
    case reflect.Uint64: fc.Print(fmt.Sprintf("uint64:%v", e))
    case reflect.Uintptr: fc.Print(fmt.Sprintf("uintptr:%v", e))
    case reflect.Float32: fc.Print(fmt.Sprintf("float32:%v", e))
    case reflect.Float64: fc.Print(fmt.Sprintf("float64:%v", e))
    case reflect.Complex64: fc.Print(fmt.Sprintf("(complex64 %f %f)", real(rv.Complex()), imag(rv.Complex())))
    case reflect.Complex128:
         fc.Print(fmt.Sprintf("(complex128 %f %f)", real(rv.Complex()), imag(rv.Complex())))
    case reflect.String:
        fc.Print(fmt.Sprintf("%q", rv.String()))
    }
    return nil
}

Saya juga mencoba membuat semacam 'Hasil' seperti jenis

type Result(type T) struct {
    Value T
    Err error
}

func NewResult(type T)(value T, err error) Result(T) {
    return Result(T){
        Value: value,
        Err: err,
    }
}

func then(type T, R)(r Result(T), f func(T) R) Result(R) {
    if r.Err != nil {
        return Result(R){Err: r.Err}
    }

    v := f(r.Value)
    return  Result(R){
        Value: v,
        Err: nil,
    }
}

func thenTry(type T, R)(r Result(T), f func(T)(R, error)) Result(R) {
    if r.Err != nil {
        return Result(R){Err: r.Err}
    }

    v, err := f(r.Value)
    return  Result(R){
        Value: v,
        Err: err,
    }
}

misalnya

    r := NewResult(GetInput())
    r2 := thenTry(r, UppercaseAndErr)
    r3 := thenTry(r2, strconv.Atoi)
    r4 := then(r3, Add5)
    if r4.Err != nil {
        // handle err
    }
    return r4.Value, nil

Idealnya Anda memiliki fungsi then menjadi metode pada tipe Hasil.

Juga contoh perbedaan absolut dalam konsep tampaknya tidak dapat dikompilasi.
Saya pikir berikut ini:

func (a ComplexAbs(T)) Abs() T {
    r := float64(real(a))
    i := float64(imag(a))
    d := math.Sqrt(r * r + i * i)
    return T(complex(d, 0))
}

seharusnya:

func (a ComplexAbs(T)) Abs() ComplexAbs(T) {
    r := float64(real(a))
    i := float64(imag(a))
    d := math.Sqrt(r * r + i * i)
    return ComplexAbs(T)(complex(d, 0))
}

Saya memiliki sedikit kekhawatiran tentang kemampuan untuk menggunakan beberapa contract untuk mengikat satu jenis parameter.

Di Scala, adalah umum untuk mendefinisikan fungsi seperti:

def compute[A: PointLike: HasTime: IsWGS](points: Vector[A]): Map[Int, A] = ???

PointLike , HasTime dan IsWGS adalah contract kecil (Scala menyebutnya type class ).

Karat juga memiliki mekanisme serupa:

fn f<F: A + B>(a F) {}

Dan kita dapat menggunakan antarmuka anonim saat mendefinisikan suatu fungsi.

type I1 interface {
    A()
}
type I2 interface {
    B()
}
func f(a interface{
    I1
    I2
})

IMO, antarmuka anonim adalah praktik yang buruk, karena interface adalah tipe nyata , pemanggil fungsi ini mungkin harus mendeklarasikan variabel dengan tipe ini. Tapi contract hanya batasan pada parameter tipe, pemanggil selalu bermain dengan beberapa tipe nyata atau hanya parameter tipe lain, saya pikir aman untuk mengizinkan kontrak anonim dalam definisi fungsi.

Untuk pengembang perpustakaan, tidak nyaman untuk mendefinisikan contract baru jika kombinasi beberapa kontrak hanya digunakan di beberapa tempat, itu akan mengacaukan basis kode. Bagi pengguna perpustakaan, mereka perlu menggali definisi untuk mengetahui persyaratan sebenarnya. Jika pengguna mendefinisikan banyak fungsi untuk memanggil fungsi di perpustakaan, mereka dapat menentukan kontrak bernama agar mudah digunakan, dan mereka bahkan dapat menambahkan lebih banyak kontrak ke kontrak baru ini jika mereka membutuhkannya karena ini valid

contract C1(T) {
    T A()
}
contract C2(T) {
    T B()
}
contract C3(T) {
    T C()
}

contract PART(T) {
    C1(T)
    C2(T)
}

contract ALL(T) {
    C1(T)
    C2(T)
    C3(T)
}

func f1(type A PART) (a A) {}

func f2(type A ALL) (a A) {
    f1(a)
}

Saya telah mencoba ini pada kompiler draft, semuanya tidak dapat diperiksa tipenya.

func f(type A C1, C2)(x A)

func f1(type A contract C(A1) {
    C1(A)
    C2(A)
}) (x A)

func f2(type A ((type A1) interface {
    I1(A1)
    I2(A1)
})(A)) (x A)

Menurut catatan di CL

Parameter tipe yang dibatasi oleh beberapa kontrak tidak akan mendapatkan tipe terikat yang benar.

Saya pikir cuplikan aneh ini valid setelah masalah ini diselesaikan

func f1(type A C1, _ C2(A)) (x A)

Berikut adalah beberapa pemikiran saya:

  • Jika kita memperlakukan contract sebagai tipe parameter tipe, type a A <=> var a A , kita dapat menambahkan gula sintaks seperti type a { A1(a); A2(a) } untuk mendefinisikan anonim berkontraksi dengan cepat.
  • Jika tidak, kita dapat memperlakukan bagian terakhir dari daftar tipe adalah daftar persyaratan, type a, b, A1(a), A2(a), A3(a, b) , gaya ini seperti menggunakan interface untuk membatasi parameter tipe.

@bobotu Sudah umum di Go untuk membuat fungsionalitas menggunakan penyematan. Tampaknya wajar untuk membuat kontrak dengan cara yang sama seperti Anda melakukannya dengan struct atau antarmuka.

@azunymous Secara pribadi saya tidak tahu bagaimana perasaan saya tentang seluruh komunitas Go yang berubah dari beberapa pengembalian menjadi Result , meskipun tampaknya proposal Kontrak akan memungkinkan ini sampai tingkat tertentu. Tim Go tampaknya menghindar dari perubahan bahasa yang membahayakan "rasa" bahasa, yang saya setujui, tetapi itu sepertinya salah satu dari perubahan itu.

Hanya pemikiran saja; Saya ingin tahu apakah ada yang mengambil poin ini.

@toolbox Saya tidak berpikir itu benar-benar mungkin untuk menggunakan sesuatu seperti satu Result secara ekstensif di luar kasus di mana Anda hanya melewati nilai, kecuali jika Anda memiliki massa generik Result s dan fungsi dari setiap kombinasi jumlah parameter dan tipe pengembalian. Dengan banyak fungsi bernomor atau menggunakan penutupan, Anda akan kehilangan keterbacaan.

Saya pikir kemungkinan besar Anda akan melihat sesuatu yang setara dengan errWriter di mana Anda akan menggunakan sesuatu seperti itu sesekali ketika cocok, dinamai dengan use case.

Secara pribadi saya tidak tahu bagaimana perasaan saya tentang seluruh komunitas Go yang berubah dari beberapa pengembalian ke Hasil

Saya tidak berpikir ini akan terjadi. Seperti yang dikatakan @azunymous , banyak fungsi memiliki beberapa tipe pengembalian dan kesalahan tetapi hasilnya tidak dapat berisi semua nilai yang dikembalikan lainnya secara bersamaan. Polimorfisme parametrik bukan satu-satunya fitur yang diperlukan untuk melakukan sesuatu seperti ini; Anda juga perlu tupel dan destructuring.

Terima kasih! Seperti yang saya katakan, bukan sesuatu yang saya pikirkan secara mendalam tetapi bagus untuk mengetahui bahwa kekhawatiran saya salah tempat.

@toolbox Saya tidak bermaksud untuk memperkenalkan beberapa sintaks baru, masalah utama di sini adalah kurangnya kemampuan untuk menggunakan kontrak anonim seperti antarmuka anonim.

Dalam draft compiler, sepertinya tidak mungkin untuk menulis sesuatu seperti ini. Kita dapat menggunakan antarmuka anonim dalam definisi fungsi, tetapi kita tidak dapat melakukan hal yang sama untuk kontrak bahkan dalam gaya verbose.

func f1(type A, B, C, D contract {
    C1(A)
    C2(A, B)
    C3(A, C)
}) (a A, b B, c C, d D)

// Or a more verbose style

func f2(type A, B, C, D (contract (_A, _B, _C) {
    C1(_A)
    C2(_A, _B)
    C3(_A, _C)
})(A, B, C)) (a A, b B, c C, d D)

IMO, ini adalah perpanjangan alami dari sintaks yang ada. Ini masih merupakan kontrak di akhir daftar parameter tipe, dan kami masih menggunakan penyematan untuk menyusun fungsionalitas. Jika Go dapat menyediakan gula untuk menghasilkan parameter tipe kontrak secara otomatis seperti cuplikan pertama, kode akan lebih mudah dibaca dan ditulis.

func fff(type A C1(A), B C2(B, A), C C3(B, C, A)) (a A, b B, c C)

// is more verbose than

func fff(type A, B, C contract {
    C1(A)
    C2(B, A)
    C3(B, C, A)
}) (a A, b B, c C)

Saya menemui beberapa masalah ketika saya mencoba mengimplementasikan iterator yang malas tanpa memanggil metode dinamis, seperti halnya Rust's Iterator.

Saya ingin mendefinisikan kontrak Iterator sederhana

contract Iterator(T, E) {
    T Next() (E, bool)
}

Karena Go tidak memiliki konsep type member , saya perlu mendeklarasikan E sebagai parameter tipe input.

Fungsi untuk mengumpulkan hasil

func Collect(type I, E Iterator) (input I) []E {
    var results []E
    for {
        e, ok := input.Next()
        if !ok {
            return results
        }
        results = append(results, e)
    }
}

Fungsi untuk memetakan elemen

contract MapIO(I, E, O, R) {
    Iterator(I, E)
    Iterator(O, R)
}

func Map(type I, E, O, R MapIO) (input I, f func (e E) R) O {
    return &lazyIterator(I, E, R){
        parent: input,
        f:      f,
    }
}

Saya punya dua masalah di sini:

  1. Saya tidak dapat mengembalikan lazyIterator di sini, kompiler mengatakan cannot convert &(lazyIterator(I, E, R) literal) (value of type *lazyIterator(I, E, R)) to O .
  2. Saya perlu mendeklarasikan kontrak baru bernama MapIO yang membutuhkan 4 baris sedangkan Map hanya membutuhkan 6 baris. Sulit bagi pengguna untuk membaca kode.

Misalkan Map dapat diketik, saya harap saya dapat menulis sesuatu seperti

type staticIterator(type E) struct {
    elem []E
}

func (it *(staticIterator(E))) Next() (E, bool) { panic("todo") }

func main() {
    inpuit := &staticIterator{
        elem: []int{1, 2, 3, 4},
    }
    mapped := Map(input, func (i int) float32 { return float32(i + 1) })
    fmt.Printf("%v\n", Collect(mapped))
}

Sayangnya, compiler mengeluh tentang hal itu tidak dapat menyimpulkan jenis. Berhenti untuk mengeluh ini setelah saya mengubah kode menjadi

func main() {
    input := &staticIterator(int){
        elem: []int{1, 2, 3, 4},
    }
    mapped := Map(*staticIterator(int), int, *lazyIterator(*staticIterator(int), int, float32), float32)(input, func (i int) float32 { return float32(i + 1) })
    result := Collect(*lazyIterator(*staticIterator(int), int, float32), float32)(mapped)
    fmt.Printf("%v\n", result)
}

Kode ini sangat sulit untuk dibaca dan ditulis, dan terlalu banyak petunjuk tipe yang diduplikasi.

BTW, kompiler akan panik dengan:

panic: interface conversion: ast.Expr is *ast.ParenExpr, not *ast.CallExpr

goroutine 1 [running]:
go/go2go.(*translator).instantiateTypeDecl(0xc000251950, 0x0, 0xc0001af860, 0xc0001a5dd0, 0xc00018ac90, 0x1, 0x1, 0xc00018bca0, 0x1, 0x1, ...)
        /home/tuzi/go-tip/src/go/go2go/instantiate.go:191 +0xd49
go/go2go.(*translator).translateTypeInstantiation(0xc000251950, 0xc000189380)
        /home/tuzi/go-tip/src/go/go2go/rewrite.go:671 +0x3f3
go/go2go.(*translator).translateExpr(0xc000251950, 0xc000189380)
        /home/tuzi/go-tip/src/go/go2go/rewrite.go:518 +0x501
go/go2go.(*translator).translateExpr(0xc000251950, 0xc0001af990)
        /home/tuzi/go-tip/src/go/go2go/rewrite.go:496 +0xe3
go/go2go.(*translator).translateExpr(0xc000251950, 0xc00018ace0)
        /home/tuzi/go-tip/src/go/go2go/rewrite.go:524 +0x1c3
go/go2go.(*translator).translateExprList(0xc000251950, 0xc00018ace0, 0x1, 0x1)
        /home/tuzi/go-tip/src/go/go2go/rewrite.go:593 +0x45
go/go2go.(*translator).translateStmt(0xc000251950, 0xc000189840)
        /home/tuzi/go-tip/src/go/go2go/rewrite.go:419 +0x26a
go/go2go.(*translator).translateBlockStmt(0xc000251950, 0xc00018d830)
        /home/tuzi/go-tip/src/go/go2go/rewrite.go:380 +0x52
go/go2go.(*translator).translateFuncDecl(0xc000251950, 0xc0001c0390)
        /home/tuzi/go-tip/src/go/go2go/rewrite.go:373 +0xbc
go/go2go.(*translator).translate(0xc000251950, 0xc0001b0400)
        /home/tuzi/go-tip/src/go/go2go/rewrite.go:301 +0x35c
go/go2go.rewriteAST(0xc000188280, 0xc000188240, 0x0, 0x0, 0xc0001f6280, 0xc0001b0400, 0x1, 0xc000195360, 0xc0001f6280)
        /home/tuzi/go-tip/src/go/go2go/rewrite.go:122 +0x101
go/go2go.RewriteBuffer(0xc000188240, 0x7ffe07d6c027, 0xa, 0xc0001ec000, 0x4fe, 0x6fe, 0x0, 0xc00011ed58, 0x40d288, 0x30, ...)
        /home/tuzi/go-tip/src/go/go2go/go2go.go:132 +0x2c6
main.translateFile(0xc000188240, 0x7ffe07d6c027, 0xa)
        /home/tuzi/go-tip/src/cmd/go2go/translate.go:26 +0xa9
main.main()
        /home/tuzi/go-tip/src/cmd/go2go/main.go:64 +0x434

Saya juga merasa tidak mungkin untuk mendefinisikan fungsi yang berfungsi dengan Iterator mengembalikan tipe tertentu.

type User struct {}

func UpdateUsers(type A Iterator(A, User)) (it A) bool { 
    // Access `User`'s field.
}

// And I found this may be possible

contract checkInts(A, B) {
    Iterator(A, B)
    B int
}

func CheckInts(type A, B checkInts) (it A) bool { panic("todo") }

Cuplikan kedua dapat berfungsi dalam beberapa skenario, tetapi sulit untuk dipahami dan tipe B yang tidak digunakan tampak aneh.

Memang, kita dapat menggunakan antarmuka untuk menyelesaikan tugas ini.

type Iterator(type E) interface {
    Next() (E, bool)
}

Saya hanya mencoba mengeksplorasi seberapa ekspresif desain Go.

BTW, kode Rust yang saya maksud adalah

fn main() {
    let input = vec![1, 2, 3, 4];
    let mapped = input.iter().map(|x| x * 3);
    let result = f(mapped);
    println!("{:?}", result.collect::<Vec<_>>());
}

fn f<I: Iterator<Item = i32>>(it: I) -> impl Iterator<Item = f32> {
    it.map(|i| i as f32 * 2.0)
}

// The definition of `map` in stdlib is
pub struct Map<I, F> {
    iter: I,
    f: F,
}

fn map<B, F: FnMut(Self::Item) -> B>(self, f: F) -> Map<Self, F>

Berikut ini ringkasan untuk https://github.com/golang/go/issues/15292#issuecomment -633233479

  1. Kami mungkin membutuhkan sesuatu untuk diungkapkan existential type untuk func Collect(type I, E Iterator) (input I) []E

    • Jenis sebenarnya dari parameter terukur universal E tidak dapat disimpulkan, karena hanya muncul di daftar pengembalian. Karena kurangnya type member untuk membuat E ada secara default, saya pikir kita mungkin menemui masalah ini di banyak tempat.

    • Mungkin kita bisa menggunakan existential type paling sederhana seperti wildcard Java ? untuk menyelesaikan inferensi tipe func Consume(type I, E Iterator) (input I) . Kita dapat menggunakan _ untuk menggantikan E , func Consume(type I Iterator(I, _)) (input I) .

    • Tetapi itu masih tidak dapat membantu masalah inferensi tipe untuk Collect , saya tidak tahu apakah sulit untuk menyimpulkan E , tetapi Rust tampaknya dapat melakukan ini.

    • Atau kita dapat menggunakan _ sebagai pengganti untuk tipe yang dapat disimpulkan oleh kompiler, dan mengisi tipe yang hilang secara manual, seperti Collect(_, float32) (...) untuk melakukan pengumpulan pada iterator float32.

  1. Karena kurangnya kemampuan untuk mengembalikan existential type , kami juga memiliki masalah untuk hal-hal seperti func Map(type I, E, O, R MapIO) (input I, f func (e E) R) O

    • Rust mendukung ini dengan menggunakan impl Iterator<E> . Jika Go dapat memberikan sesuatu seperti ini, kami dapat mengembalikan iterator baru tanpa boxing, mungkin berguna untuk beberapa kode kinerja-kritis.

    • Atau kita bisa mengembalikan objek kotak, beginilah cara Rust menyelesaikan masalah ini sebelum mendukung existential type pada posisi pengembalian. Tapi pertanyaannya adalah hubungan antara contract dan interface , mungkin kita perlu mendefinisikan beberapa aturan konversi dan membiarkan compiler mengonversinya secara otomatis. Jika tidak, kita mungkin perlu mendefinisikan contract dan interface dengan metode yang sama untuk kasus ini.

    • Jika tidak, kita hanya dapat menggunakan CPS untuk memindahkan parameter tipe dari posisi kembali ke daftar input. misalnya func Map(type I, E, O, R MapIO) (input I, f func (e E) R, f1 func (outout O)) . Tapi ini tidak berguna dalam praktiknya, hanya karena kita harus menulis tipe sebenarnya dari O ketika kita meneruskan fungsi ke Map .

Saya baru saja mengikuti diskusi ini sedikit, dan tampaknya cukup jelas bahwa kesulitan sintaksis dengan parameter tipe tetap menjadi kesulitan besar dengan draf proposal. Ada cara untuk menghindari parameter tipe sepenuhnya dan mencapai sebagian besar fungsi generik: #32863 -- mungkin ini saat yang tepat untuk mempertimbangkan alternatif tersebut sehubungan dengan beberapa diskusi lebih lanjut ini? Jika ada kemungkinan sesuatu seperti desain ini diadopsi, saya akan dengan senang hati mencoba memodifikasi taman bermain perakitan web untuk memungkinkan pengujiannya.

Perasaan saya adalah bahwa fokus saat ini adalah memakukan kebenaran semantik proposal saat ini, terlepas dari sintaksnya, karena semantik sangat sulit untuk diubah.

Saya baru saja melihat makalah tentang Featherweight Go diterbitkan di Arxiv dan merupakan kolaborasi antara tim Go dan beberapa pakar teori tipe. Sepertinya ada lebih banyak makalah yang direncanakan dalam nada ini.

Untuk menindaklanjuti komentar saya sebelumnya, Phil Wadler dari Haskell ketenaran dan salah satu penulis di kertas memiliki pembicaraan yang dijadwalkan pada "Featherweight Go" pada Senin, 8 Juni @ 7 pagi PDT / 10 pagi EDT: http://chalmersfp.org/ . tautan youtube

@rcoreilly Saya pikir kita hanya akan tahu apakah "kesulitan sintaksis" adalah masalah besar ketika orang memiliki lebih banyak pengalaman menulis dan, yang lebih penting, membaca kode yang ditulis sesuai dengan rancangan desain. Kami sedang mencari cara agar orang-orang mencobanya.

Dengan tidak adanya itu, saya pikir sintaksnya hanyalah apa yang dilihat orang pertama dan dikomentari terlebih dahulu. Ini mungkin masalah besar, mungkin tidak. Kami belum tahu.

Untuk menindaklanjuti komentar saya sebelumnya, Phil Wadler dari Haskell yang terkenal dan salah satu penulis di atas kertas memiliki jadwal ceramah di "Featherweight Go" pada hari Senin

Pembicaraan oleh Phil Wadler sangat mudah didekati dan menarik. Saya kesal pada batas waktu satu jam yang tampaknya tidak ada gunanya yang mencegahnya melakukan monomorfisasi.

Perlu dicatat bahwa Wadler diminta oleh Pike untuk masuk; ternyata mereka saling kenal dari Bell Labs. Bagi saya, Haskell memiliki seperangkat nilai dan paradigma yang sangat berbeda, dan menarik untuk melihat bagaimana (pencipta? desainer utama?) berpikir tentang Go dan generik di Go.

Proposal itu sendiri memiliki sintaks yang sangat mirip dengan Kontrak, tetapi menghilangkan Kontrak itu sendiri, hanya menggunakan parameter tipe dan antarmuka. Perbedaan utama yang disebut adalah kemampuan untuk mengambil tipe generik dan mendefinisikan metode di atasnya yang memiliki batasan lebih spesifik daripada tipe itu sendiri.

Rupanya Tim Go sedang mengerjakan atau memiliki prototipe ini! Itu akan menarik. Sementara itu, bagaimana tampilannya?

package graph

type Node(type e) interface{
    Edges() []e
}

type Edge(type n) interface{
    Nodes() (from n, to n)
}

type Graph(type n Node(e), e Edge(n)) struct { ... }
func New(type n Node(e), e Edge(n))(nodes []n) *Graph(n, e) { ... }
func (g *Graph(type n Node(e), e Edge(n))) ShortestPath(from, to n) []e { ... }

Apakah saya memiliki hak itu? Aku pikir begitu. Jika saya melakukannya ... tidak buruk, sebenarnya. Tidak cukup menyelesaikan masalah tanda kurung yang gagap, tetapi tampaknya entah bagaimana membaik. Beberapa gejolak tanpa nama dalam diriku menjadi tenang.

Bagaimana dengan contoh tumpukan dari @urandom ? (Aliasing interface{} menjadi Any dan menggunakan inferensi tipe dalam jumlah tertentu.)

package main

type Any interface{}

type Stack(type t Any) []t

func (s Stack(type t Any)) Peek() t {
    return s[len(s)-1]
}

func (s *Stack(type t Any)) Pop() {
    *s = (*s)[:len(*s)-1]
}

func (s *Stack(type t Any)) Push(value t) {
    *s = append(*s, value)
}

type StackIterator(type t Any) struct{
    stack Stack(t)
    current int
}

func (s *Stack(type t Any)) Iter() *StackIterator(t) {
    it := StackIterator(t){stack: *s, current: len(*s)}

    return &it
}

func (i *StackIterator(type t Any)) Next() (bool) { 
    i.current--

    if i.current < 0 { 
        return false
    }

    return true
}

func (i *StackIterator(type t Any)) Value() t {
    if i.current < 0 {
        var zero t
        return zero
    }

    return i.stack[i.current]
}

type Iterator(type t Any) interface {
    Next() bool
    Value() t
}

func Map(type t Any, u Any)(it Iterator(t), mapF func(t) u) Iterator(u) {
    return mapIt(t, u){it, mapF}
}

type mapIt(type t Any, u Any) struct {
    parent Iterator(t)
    mapF func(t) u
}

func (i mapIt(type t Any, u Any)) Next() bool {
    return i.parent.Next()
}

func (i mapIt(type t Any, u Any)) Value() u {
    return i.mapF(i.parent.Value())
}

func Filter(type t Any)(it Iterator(t), predicate func(t) bool) Iterator(t) {
    return filter(t){it, predicate}
}

type filter(type t Any) struct {
    parent Iterator(t)
    predicateF func(t) bool
}

func (i filter(type t Any)) Next() bool {
    if !i.parent.Next() {
        return false
    }

    n := true
    for n && !i.predicateF(i.parent.Value()) {
        n = i.parent.Next()
    }

    return n
}

func (i filter(type t Any)) Value() t {
    return i.parent.Value()
}

func Distinct(type t comparable)(it Iterator(t)) Iterator(t) {
    return distinct(t){it, map[t]struct{}{}}
}

type distinct(type t comparable) struct {
    parent Iterator(t)
    set map[t]struct{}
}

func (i distinct(type t Any)) Next() bool {
    if !i.parent.Next() {
        return false
    }

    n := true
    for n {
        _, ok := i.set[i.parent.Value()]
        if !ok {
            i.set[i.parent.Value()] = struct{}{}
            break
        }
        n = i.parent.Next()
    }


    return n
}

func (i distinct(type t Any)) Value() t {
    return i.parent.Value()
}

func ToSlice(type t Any)(it Iterator(t)) []t {
    var res []t

    for it.Next() {
        res = append(res, it.Value())
    }

    return res
}

func ToSet(type t comparable)(it Iterator(t)) map[t]struct{} {
    var res map[t]struct{}

    for it.Next() {
        res[it.Value()] = struct{}{}
    }

    return res
}

func Reduce(type t Any)(it Iterator(t), id t, acc func(a, b t) t) t {
    for it.Next() {
        id = acc(id, it.Value())
    }

    return id
}

func main() {
    var stack Stack(string)
    stack.Push("foo")
    stack.Push("bar")
    stack.Pop()
    stack.Push("alpha")
    stack.Push("beta")
    stack.Push("foo")
    stack.Push("gamma")
    stack.Push("beta")
    stack.Push("delta")


    var it Iterator(string) = stack.Iter()

    it = Filter(string)(it, func(s string) bool {
        return s == "foo" || s == "beta" || s == "delta"
    })

    it = Map(string, string)(it, func(s string) string {
        return s + ":1"
    })

    it = Distinct(string)(it)

    println(Reduce(it, "", func(a, b string) string {
        if a == "" {
            return b
        }
        return a + ":" + b
    }))


}

Sesuatu seperti itu, kurasa. Saya menyadari sebenarnya tidak ada Kontrak dalam kode itu, jadi ini bukan representasi yang baik tentang bagaimana itu ditangani dalam gaya FGG, tapi saya bisa mengatasinya sebentar lagi.

Tayangan:

  • Saya suka memiliki gaya parameter tipe dalam metode yang cocok dengan deklarasi tipe. Yaitu mengatakan "ketik" dan secara eksplisit menyatakan jenisnya, ("type" param paramType, param paramType...) daripada (param, param) . Itu membuatnya konsisten secara visual, sehingga kodenya lebih mudah dilihat.
  • Saya suka memiliki parameter tipe menjadi huruf kecil. Variabel satu huruf di Go menunjukkan penggunaan yang sangat lokal, tetapi kapitalisasi berarti itu diekspor, dan mereka tampak bertentangan saat disatukan. Huruf kecil terasa lebih baik karena parameter tipe dicakupkan ke fungsi/tipe.

Oke, bagaimana dengan kontrak?

Nah, satu hal yang saya suka adalah Stringer tidak tersentuh; Anda tidak akan memiliki antarmuka Stringer dan kontrak Stringer .

type Stringer interface {
    String() string
}

func Stringify(type t Stringer)(s []t) (ret []string) {
    for _, v := range s {
        ret = append(ret, v.String())
    }
    return ret
}

Kami juga memiliki contoh viaStrings :

type ToString interface {
    Set(string)
}

type FromString interface {
    String() string
}

func SetViaStrings(type to ToString, from FromString)(s []from) []to {
    r := make([]to, len(s))
    for i, v := range s {
        r[i].Set(v.String())
    }
    return r
}

Menarik. Saya tidak benar-benar 100% yakin apa yang diperoleh kontrak kami dalam kasus itu. Mungkin sebagian darinya adalah aturan bahwa suatu fungsi dapat memiliki beberapa parameter tipe tetapi hanya satu kontrak.

Persamaan tercakup dalam makalah/pembicaraan:

contract equal(T) {
    T Equal(T) bool
}

// becomes

type equal(type t equal(t)) interface{
    Equal(t) bool
}

Dan seterusnya. Saya cukup tertarik dengan semantik. Parameter tipe adalah antarmuka, jadi aturan yang sama tentang penerapan antarmuka diterapkan pada apa yang dapat digunakan sebagai parameter tipe. Itu tidak "dikotak" saat runtime - kecuali jika Anda secara eksplisit memberikannya sebuah antarmuka, saya kira, yang Anda bebas untuk melakukannya.

Hal terbesar yang saya perhatikan sebagai tidak tercakup adalah pengganti kemampuan Kontrak untuk menentukan berbagai tipe primitif. Yah, saya yakin strategi untuk itu, dan banyak hal lainnya, akan datang :

8 - KESIMPULAN

Ini adalah awal dari cerita, bukan akhir. Dalam pekerjaan mendatang, kami berencana untuk melihat metode implementasi lain selain monomorfisasi, dan khususnya untuk mempertimbangkan implementasi berdasarkan representasi tipe runtime yang lewat, mirip dengan yang digunakan untuk generik .NET. Pendekatan campuran yang terkadang menggunakan monomorfisasi dan melewatkan representasi runtime terkadang mungkin yang terbaik, sekali lagi mirip dengan yang digunakan untuk .NET generik.

Featherweight Go terbatas pada subset kecil dari Go. Kami merencanakan model fitur penting lainnya seperti tugas, larik, irisan, dan paket, yang akan kami beri nama Bantamweight Go; dan model mekanisme konkurensi inovatif Go berdasarkan "goroutines" dan penyampaian pesan, yang akan kami beri nama Cruiserweight Go.

Featherweight Go tampak hebat bagi saya. Ide bagus untuk melibatkan beberapa pakar teori tipe. Ini terlihat lebih seperti jenis hal yang saya anjurkan lebih lanjut untuk topik ini.

Senang mendengar bahwa para ahli teori tipe secara aktif mengerjakan ini!

Bahkan terlihat mirip (kecuali untuk sintaks yang sedikit berbeda) dengan proposal lama saya "kontrak adalah antarmuka" https://github.com/cosmos72/gomacro/blob/master/doc/generics-cti.md

@toolbox
Dengan mengizinkan metode dengan batasan yang berbeda dari tipe sebenarnya (dan juga tipe yang berbeda sama sekali), FGG membuka beberapa kemungkinan yang tidak mungkin dilakukan dengan draft kontrak saat ini. Sebagai contoh, dengan FGG, seseorang harus dapat mendefinisikan Iterator dan ReversibleIterator, dan memiliki iterator perantara dan pengakhir (peta, pengurangan filter) mendukung keduanya (misalnya, dengan Next() dan NextFromBack() untuk reversibel) , tergantung pada apa iterator induknya.

Saya pikir penting untuk diingat bahwa FGG tidak secara definitif di mana obat generik di Go akan berakhir. Ini satu mengambil mereka, dari luar. Dan itu secara eksplisit mengabaikan banyak hal yang akhirnya memperumit produk akhir. Juga, saya belum membaca koran, hanya menonton ceramahnya. Dengan mengingat hal itu: Sejauh yang saya tahu, ada dua cara signifikan di mana FGG menambahkan kekuatan ekspresif atas rancangan kontrak:

  1. Ini memungkinkan penambahan parameter tipe baru ke metode (seperti yang ditunjukkan dalam contoh "Daftar dan Peta" dalam pembicaraan). AFAICT ini akan memungkinkan penerapan Functor (sebenarnya, itu contoh Daftarnya, jika saya tidak salah), Monad dan teman-teman mereka. Saya tidak berpikir tipe spesifik itu menarik bagi Gophers, tetapi ada kasus penggunaan yang menarik untuk ini (misalnya port Go dari Flume atau konsep serupa kemungkinan akan mendapat manfaat). Secara pribadi, saya merasakan perubahan yang positif, meskipun saya belum melihat apa implikasinya untuk refleksi dan sejenisnya. Saya merasa bahwa deklarasi metode yang menggunakan ini mulai sulit dibaca - terutama jika parameter tipe dari tipe generik juga harus dicantumkan di penerima.
  2. Ini memungkinkan parameter tipe untuk memiliki batasan yang lebih ketat pada metode tipe generik daripada tipe itu sendiri. Seperti yang disebutkan oleh orang lain, ini memungkinkan Anda untuk memiliki tipe generik yang sama mengimplementasikan metode yang berbeda, tergantung pada tipe apa yang dipakai. Saya tidak yakin ini adalah perubahan yang baik, secara pribadi. Tampaknya resep untuk kebingungan, membuat Map(int, T) berakhir dengan metode yang tidak dimiliki Map(string, T) . Paling tidak, kompiler perlu memberikan pesan kesalahan yang sangat baik, jika hal seperti ini terjadi. Sementara itu, manfaatnya tampaknya relatif kecil - terutama mengingat bahwa faktor pendorong dari pembicaraan (kompilasi terpisah) tidak terlalu relevan dengan Go: Karena metode harus dideklarasikan dalam paket yang sama dengan jenis penerimanya dan mengingat bahwa paket adalah unitnya kompilasi, Anda tidak dapat benar-benar memperluas jenisnya secara terpisah. Saya tahu bahwa berbicara tentang kompilasi lebih merupakan cara konkret untuk membicarakan manfaat yang lebih abstrak, tetapi tetap saja, saya tidak merasa manfaat itu banyak membantu Go.

Saya menantikan langkah selanjutnya, dalam hal apa pun :)

Saya pikir penting untuk diingat bahwa FGG tidak secara definitif di mana obat generik di Go akan berakhir.

@Merovius kenapa kamu berkata begitu?

@arl
FG lebih merupakan makalah penelitian tentang apa yang _bisa_ dilakukan. Tidak ada yang mengatakan secara eksplisit bahwa ini adalah bagaimana polimorfisme akan bekerja di Go di masa depan. Meskipun pengembang inti 2 Go terdaftar sebagai penulis di makalah, itu tidak berarti bahwa ini akan diterapkan di Go.

Saya pikir penting untuk diingat bahwa FGG tidak secara definitif di mana obat generik di Go akan berakhir. Ini satu mengambil mereka, dari luar. Dan itu secara eksplisit mengabaikan banyak hal yang akhirnya memperumit produk akhir.

Ya, poin yang sangat bagus.

Juga, saya akan perhatikan bahwa Wadler bekerja sebagai bagian dari tim, dan produk yang dihasilkan dibangun di atas dan sangat dekat dengan proposal Kontrak, yang merupakan hasil kerja bertahun-tahun dari pengembang inti.

Dengan mengizinkan metode dengan batasan yang berbeda dari tipe sebenarnya (dan juga tipe yang berbeda sama sekali), FGG membuka beberapa kemungkinan yang tidak mungkin dilakukan dengan draft kontrak saat ini. ...

@urandom Saya ingin tahu seperti apa contoh Iterator itu; maukah kamu melempar sesuatu bersama?

Secara terpisah, saya tertarik pada apa yang generik dapat lakukan di luar peta dan filter dan hal-hal fungsional, dan lebih ingin tahu bagaimana mereka dapat menguntungkan proyek seperti k8s. (Bukannya mereka akan pergi dan melakukan refactor pada saat ini, tetapi saya telah mendengar secara anekdot bahwa kurangnya obat generik memerlukan beberapa gerak kaki yang mewah, saya pikir dengan Sumber Daya Kustom? Seseorang yang lebih akrab dengan proyek ini dapat mengoreksi saya.)

Saya merasa bahwa deklarasi metode yang menggunakan ini mulai sulit dibaca - terutama jika parameter tipe dari tipe generik juga harus dicantumkan di penerima.

Mungkin gofmt bisa membantu? Mungkin kita perlu menggunakan multi-line. Layak bermain-main, mungkin.

Seperti yang disebutkan oleh orang lain, ini memungkinkan Anda untuk memiliki tipe generik yang sama mengimplementasikan metode yang berbeda, tergantung pada tipe apa yang dipakai.

Saya mengerti apa yang Anda katakan @Merovius

Itu dipanggil oleh Wadler sebagai perbedaan, dan itu memungkinkan dia menyelesaikan Masalah Ekspresinya, tetapi Anda membuat poin yang bagus bahwa jenis paket kedap udara Go tampaknya membatasi apa yang dapat/harus Anda lakukan dengan ini. Dapatkah Anda memikirkan kasus aktual di mana Anda ingin melakukan itu?

Seperti yang disebutkan oleh orang lain, ini memungkinkan Anda untuk memiliki tipe generik yang sama mengimplementasikan metode yang berbeda, tergantung pada tipe apa yang dipakai.

Saya mengerti apa yang Anda katakan @Merovius

Itu dipanggil oleh Wadler sebagai perbedaan, dan itu memungkinkan dia menyelesaikan Masalah Ekspresinya, tetapi Anda membuat poin yang bagus bahwa jenis paket kedap udara Go tampaknya membatasi apa yang dapat/harus Anda lakukan dengan ini. Dapatkah Anda memikirkan kasus aktual di mana Anda ingin melakukan itu?

Ironisnya, pemikiran pertama saya adalah bahwa itu dapat digunakan untuk menyelesaikan beberapa tantangan yang dijelaskan dalam artikel ini: https://blog.merovius.de/2017/07/30/the-trouble-with-optional-interfaces.html

@toolbox

Secara terpisah, saya tertarik pada apa yang dapat dilakukan obat generik selain peta dan filter serta hal-hal fungsional,

FWIW, harus diklarifikasi bahwa ini adalah jenis penjualan singkat "peta dan filter dan hal-hal fungsional". Saya pribadi tidak ingin map dan filter melebihi struktur data bawaan dalam kode saya, misalnya (saya lebih suka for-loop). Tapi itu juga bisa berarti

  1. Menyediakan akses umum ke struktur data pihak ketiga mana pun . yaitu map dan filter dapat dibuat untuk bekerja di atas pohon generik, atau peta yang diurutkan, atau… juga. Jadi, Anda dapat menukar apa yang dipetakan, untuk lebih banyak kekuatan. Dan yang lebih penting
  2. Anda dapat menukar bagaimana itu dipetakan. Misalnya, Anda dapat membuat versi Compose yang dapat memunculkan beberapa goroutine untuk setiap fungsi dan menjalankannya secara bersamaan, menggunakan saluran. Ini akan memudahkan untuk menjalankan pipeline pemrosesan data secara bersamaan dan meningkatkan bottle-neck secara otomatis, sementara hanya perlu menulis func(A) B s. Atau Anda dapat menempatkan fungsi yang sama ke dalam kerangka kerja yang menjalankan ribuan salinan program dalam sebuah cluster, menjadwalkan kumpulan data di antara mereka (itulah yang saya singgung ketika saya menautkan ke Flume di atas).

Jadi, meskipun dapat menulis Map dan Filter dan Reduce mungkin tampak membosankan di permukaan, teknik yang sama membuka beberapa kemungkinan yang sangat menarik untuk membuat komputasi terukur lebih mudah.

@ChrisHines

Ironisnya, pemikiran pertama saya adalah bahwa itu dapat digunakan untuk menyelesaikan beberapa tantangan yang dijelaskan dalam artikel ini: https://blog.merovius.de/2017/07/30/the-trouble-with-optional-interfaces.html

Ini adalah pemikiran yang menarik dan tentu saja terasa seperti seharusnya. Tapi saya belum melihat caranya. Jika Anda mengambil contoh ResponseWriter , tampaknya ini memungkinkan Anda untuk menulis pembungkus generik yang aman, dengan metode yang berbeda tergantung pada apa yang didukung ResponseWriter yang dibungkus. Tetapi, bahkan jika Anda dapat menggunakan batas yang berbeda pada metode yang berbeda, Anda tetap harus menuliskannya. Jadi, sementara itu dapat membuat situasi aman dalam arti bahwa Anda tidak menambahkan metode yang tidak Anda dukung, Anda masih perlu menghitung semua metode yang dapat Anda dukung, jadi perangkat tengah mungkin masih menutupi beberapa antarmuka opsional hanya dengan tidak mengetahui tentang mereka. Sementara itu, Anda juga dapat (bahkan tanpa fitur ini) melakukannya

type Middleware (type RW http.ResponseWriter) struct {
    RW
}

dan timpa metode selektif yang Anda pedulikan - dan promosikan semua metode RW lainnya. Jadi Anda bahkan tidak perlu menulis pembungkus dan secara transparan bahkan mendapatkan metode yang tidak Anda ketahui.

Jadi, dengan asumsi kita mendapatkan metode yang dipromosikan untuk parameter tipe yang disematkan pada struct generik (dan saya harap kita melakukannya), masalah tampaknya lebih baik diselesaikan dengan metode itu.

Saya pikir solusi spesifik untuk http.ResponseWriter adalah seperti error.Is/As . Tidak perlu ada perubahan bahasa, hanya penambahan pustaka untuk membuat metode standar pembungkus ResponseWriter dan cara menanyakan apakah salah satu ResponseWriter dalam rantai dapat menangani, egwPush. Saya skeptis bahwa obat generik akan cocok untuk sesuatu seperti ini karena intinya adalah memiliki pilihan runtime antara antarmuka opsional, misalnya Push hanya tersedia di http2 dan tidak jika saya menjalankan server dev lokal http1.

Melihat melalui Github, saya rasa saya tidak pernah membuat masalah untuk ide ini, jadi mungkin saya akan melakukannya sekarang.

Sunting: #39558.

@toolbox
Dugaan saya adalah bahwa itu akan terlihat seperti ini, bersama dengan kode monomorfisasi internalnya:

package iter

type Any interface{}

type Iterator(type T Any) interface {
    Next() bool
    Value() T
}

type ReversibleIterator(type T Any) interface {
    Iterator(T)
    NextBack() bool
}

type mapIt(type I Iterator(T), T Any, U Any) struct {
    parent I
    mapF func(T) U
}

func (i mapIt(type I Iterator(T))) Next() bool {
    return i.parent.Next()
}

func (i mapIt(type I Iterator(T), T Any, U Any)) Value() U { 
    return i.mapF(i.parent.Value())
}

func (i mapIt(type I ReversibleIterator(T))) NextBack() bool { 
    return i.parent.NextBack()
}

// Monomorphisation
type mapIt<OnlyForward, int, float64> struct {
    parent OnlyForward,
    mapF func(int) float64
}

func (i mapIt<OnlyForward, int, float64>) Next() bool {
    return i.parent.Next()
}

func (i mapIt<OnlyForward, int, float64>) Value() float64 {
    return i.mapF(i.parent.Value())
}

type mapIt<Slice, int, string> struct {
    parent Slice,
    mapF func(int) string
}

func (i mapIt<Slice, int, string>) Next() bool {
    return i.parent.Next()
}

func (i mapIt<Slice, int, string>) Value() string {
    return i.mapF(i.parent.Value())
}

func (i mapIt<Slice, int, string>) NextBack() bool {
    return i.parent.NextBack()
}



Dugaan saya adalah bahwa itu akan terlihat seperti ini, bersama dengan kode monomorfisasi internalnya:

FWIW inilah tweet saya dari beberapa tahun yang lalu yang mengeksplorasi bagaimana iterator dapat bekerja di Go dengan obat generik. Jika Anda melakukan substitusi global untuk mengganti <T> dengan (type T) , Anda mendapatkan sesuatu yang tidak jauh dari proposal saat ini: https://twitter.com/rogpeppe/status/425035488425037824

FWIW, harus diklarifikasi bahwa ini adalah jenis penjualan singkat "peta dan filter dan hal-hal fungsional". Saya pribadi tidak ingin memetakan dan memfilter struktur data bawaan dalam kode saya, misalnya (saya lebih suka for-loop). Tapi bisa juga berarti...

Saya mengerti maksud Anda dan tidak setuju, dan ya, kami akan mendapat manfaat dari hal-hal yang dicakup oleh contoh Anda.
Tapi saya masih bertanya-tanya tentang bagaimana sesuatu seperti k8s akan terpengaruh, atau basis kode lain dengan tipe data "generik" di mana jenis tindakan yang dilakukan bukan peta atau filter, atau setidaknya melampaui itu. Saya bertanya-tanya seberapa efektif Kontrak atau FGG dalam meningkatkan keamanan jenis dan kinerja dalam konteks semacam itu.

Ingin tahu apakah ada yang bisa menunjukkan basis kode, semoga lebih sederhana dari k8s, yang cocok dengan kategori semacam ini?

@urandom wah. Jadi jika Anda membuat instance mapIt dengan parent yang mengimplementasikan ReversibleIterator maka mapIt memiliki metode NextBack() dan jika tidak, itu tidak' T. Apakah saya membacanya dengan benar?

Memikirkannya, sepertinya itu berguna dari perspektif perpustakaan. Anda memiliki beberapa tipe struct generik yang cukup terbuka ( Any tipe params) dan mereka memiliki banyak metode, dibatasi oleh berbagai antarmuka. Jadi, ketika Anda menggunakan pustaka dalam kode Anda sendiri, tipe yang Anda sematkan di struct memberi Anda kemampuan untuk memanggil serangkaian metode tertentu, sehingga Anda mendapatkan serangkaian fungsionalitas pustaka tertentu. Apa set fungsionalitas itu, diketahui pada waktu kompilasi berdasarkan metode yang dimiliki tipe Anda.

...Tampaknya sedikit seperti apa yang @ChrisHines kemukakan bahwa Anda agak bisa menulis kode yang memiliki fungsionalitas lebih atau kurang berdasarkan apa yang diimplementasikan oleh tipe Anda, tetapi sekali lagi ini benar-benar masalah set metode yang tersedia meningkat atau menurun, bukan perilaku metode tunggal, jadi ya saya tidak melihat bagaimana hal pembajak http2 dibantu dengan ini.

Pokoknya sangat menarik.

Bukannya saya akan melakukan ini, tetapi saya kira ini mungkin:

type OverrideX interface {
    GetX() int
}

type OverrideY interface {
    GetY() int
}

type Inheritor(type child Any) struct {
    Parent
    c child
}

func (i Inheritor(type child OverrideX)) GetX() int {
    return i.c.GetX()
}

func (i Inheritor(type child OverrideY)) GetY() int {
    return i.c.GetY()
}

type Parent struct {
    x, y int
}

func (p Parent) GetX() int {
    return p.x
}

func (p Parent) GetY() int {
    return p.y
}

type Child struct {
    x int
}

func (c Child) GetX() int {
    return c.x
}

func main() {
    i := Inheritor(Child){Parent{5, 6}, Child{3}}
    x, y := i.GetX(), i.GetY() // 3, 6
}

Sekali lagi, sebagian besar lelucon, tapi saya pikir itu baik untuk mengeksplorasi batas dari apa yang mungkin.

Sunting: Hm, memang menunjukkan bagaimana Anda dapat memiliki set metode yang berbeda tergantung pada tipe param, tetapi menghasilkan efek yang sama persis seperti hanya menyematkan Parent di Child . Sekali lagi, contoh konyol ;)

Saya bukan penggemar berat metode yang hanya bisa dipanggil dengan tipe tertentu. Mengingat contoh @toolbox , mungkin akan sulit untuk menguji karena fakta bahwa beberapa metode hanya dapat dipanggil mengingat beberapa anak tertentu - penguji kemungkinan akan melewatkan beberapa kasus. Juga tidak jelas metode mana yang tersedia dan membutuhkan IDE untuk memberikan saran bukanlah yang seharusnya dibutuhkan Go. Namun, Anda dapat mengimplementasikan ini hanya dengan menggunakan tipe yang diberikan oleh struct dengan melakukan penegasan tipe dalam metode.

func (i Inheritor(type child Any)) GetX() int {
    if c, ok := i.c.(OverrideX); ok {
        return c.GetX()
    }
    return i.Parent.GetX()
}

func (i Inheritor(type child Any)) GetY() int {
    if c, ok := i.c.(OverrideY); ok {
        return c.GetY()
    }
    return i.Parent.GetY()
} 

Kode ini juga aman untuk tipe, jelas, mudah diuji, dan kemungkinan berjalan identik dengan aslinya tanpa kebingungan.

@TotallyGamerJet
Contoh khusus itu adalah tipe-aman, namun yang lain tidak, dan akan memerlukan kepanikan runtime dengan tipe yang tidak kompatibel.

Juga, saya tidak yakin bagaimana penguji mungkin melewatkan kasus apa pun, mengingat kemungkinan besar merekalah yang menulis kode generik di tempat pertama. Juga, apakah kejelasannya agak subjektif atau tidak, meskipun jelas tidak memerlukan IDE untuk menyimpulkannya. Perlu diingat, ini bukan fungsi overloading, metode ini bisa dipanggil atau tidak, jadi bukan berarti beberapa kasus bisa dilewati secara tidak sengaja. Siapa pun dapat melihat bahwa metode ini ada untuk jenis tertentu, dan mereka mungkin perlu membacanya lagi untuk memahami jenis apa yang diperlukan, tetapi hanya itu saja.

@urandom Saya tidak bermaksud dengan contoh spesifik itu seseorang akan melewatkan sebuah kasus - ini sangat singkat. Maksud saya ketika Anda memiliki banyak metode yang hanya dapat dipanggil dengan tipe tertentu. Jadi saya berdiri dengan tidak menggunakan subtyping (seperti yang saya suka menyebutnya). Bahkan dimungkinkan untuk memecahkan "Masalah Ekspresi" tanpa menggunakan pernyataan tipe atau subtipe. Berikut caranya:

type Any interface {}

type Evaler(type t Any) interface {
    Eval() t
}

type Num struct {
    value int
}

func (n Num) Eval() int {
    return n.value
}

type Plus(type a Evaler(type t Any)) struct {
    left a
    right a
}

func (p Plus(type a Evaler(type t Any)) Eval() t {
    return p.left.Eval() + p.right.Eval()
}

func (p Plus(type a Evaler(type t Any)) String() string {
    return fmt.Sprintf("(%s+%s)", p.left, p.right)
}

type Expr interface {
    Evaler
    fmt.Stringer
}

func main() {
    var e Expr = Plus(Num){Num{1}, Num{2}}
    var v int = e.Eval() // 3
    var s string = e.String() // "(1+2)"
}

Penyalahgunaan metode Eval harus diketahui pada waktu kompilasi karena fakta bahwa itu tidak diperbolehkan untuk memanggil Eval on Plus dengan tipe yang tidak mengimplementasikan penambahan. Meskipun, dimungkinkan untuk menggunakan String() (mungkin menambahkan struct) secara tidak benar, pengujian yang baik harus menangkap kasus-kasus tersebut. Dan Go biasanya merangkul kesederhanaan di atas "kebenaran". Satu-satunya hal yang diperoleh dengan subtipe adalah lebih banyak kebingungan dalam dokumen dan penggunaan. Jika Anda dapat memberikan contoh yang memerlukan subtipe, saya mungkin lebih cenderung berpikir itu adalah ide yang bagus tetapi saat ini, saya tidak yakin.
EDIT: Memperbaiki kesalahan dan ditingkatkan

@TotallyGamerJet dalam contoh Anda, metode String harus memanggil String secara rekursif, bukan Eval

@TotallyGamerJet dalam contoh Anda, metode String harus memanggil String secara rekursif, bukan Eval

@gaib
Saya tidak yakin apa yang Anda maksud. Tipe struct Plus adalah Evaler yang tidak memastikan bahwa fmt.Stringer terpenuhi. Memanggil String() pada kedua Evalers akan membutuhkan pernyataan tipe dan karenanya tidak menjadi typesafe.

@TotallyGamerJet
Sayangnya, itulah ide dari metode String. Itu harus secara rekursif memanggil metode String apa pun pada anggotanya, jika tidak, tidak ada gunanya. Tetapi Anda sudah melihat bahwa itu akan memerlukan pernyataan tipe dan panik jika Anda tidak dapat memastikan bahwa metode pada tipe Plug memerlukan tipe a yang memiliki metode String

@urandom
Anda benar! Cukup mengejutkan Sprintf akan melakukan pernyataan jenis itu untuk Anda. Jadi, Anda cukup mengirim di kolom kiri dan kanan. Meskipun masih bisa panik jika tipe di Plus tidak mengimplementasikan Stringer tapi saya baik-baik saja karena itu mungkin untuk menghindari kepanikan dengan menggunakan kata kerja %v untuk mencetak struct (itu akan memanggil String( ) jika tersedia). Saya pikir solusi ini jelas dan ketidakpastian lainnya harus didokumentasikan dalam kode. Jadi saya masih tidak yakin mengapa subtipe diperlukan.

@TotallyGamerJet
Saya pribadi masih gagal melihat masalah apa yang bisa muncul jika dibiarkan memiliki metode dengan kendala yang berbeda. Metodenya masih ada, dan kode dengan jelas menjelaskan argumen apa (dan penerima, dalam kasus khusus) yang diperlukan.
Sama seperti memiliki metode, menerima argumen string , atau penerima MyType , dapat dibaca dengan jelas dan tidak ambigu, demikian juga definisi berikut:

func (rec MyType(type T SomeInterface(T)) Foo() T

Persyaratan ditandai dengan jelas dalam tanda tangan itu sendiri. Yaitu MyType(type T SomeInterface(T)) dan tidak ada yang lain.

Ubah https://golang.org/cl/238003 menyebutkan masalah ini: design: add go2draft-type-parameters.md

Ubah https://golang.org/cl/238241 menyebutkan masalah ini: content: add generics-next-step article

Natal lebih awal!

  • Saya dapat melihat banyak upaya dilakukan untuk membuat dokumen desain dapat didekati, itu menunjukkan dan itu bagus dan sangat dihargai.
  • Iterasi ini adalah peningkatan besar di mata saya dan saya bisa melihat ini diimplementasikan apa adanya.
  • Setuju dengan hampir semua alasan dan logika.
  • Seperti itu jika Anda menentukan batasan untuk parameter tipe tunggal, Anda harus melakukannya untuk semua.
  • Sebanding terdengar bagus.
  • Ketik daftar di antarmuka tidak buruk; setuju itu lebih baik daripada metode operator, tetapi dalam pikiran saya itu mungkin area terbesar untuk diskusi lebih lanjut.
  • Jenis inferensi (masih) bagus.
  • Inferensi untuk batasan berparameter tipe argumen tunggal sepertinya lebih pintar daripada kejelasan.
  • Saya suka "Kami tidak mengklaim bahwa ini sederhana" dalam contoh grafik. Tidak apa-apa.
  • (type *T constraint) tampak seperti solusi yang baik untuk masalah pointer.
  • Sepenuhnya menyetujui perubahan func(x(T)) .
  • Saya pikir kita ingin inferensi tipe untuk literal komposit dari kelelawar? 😄.

Terima kasih kepada tim Go! 🎉.

https://go.googlesource.com/proposal/+/refs/heads/master/design/go2draft-type-parameters.md#comparable -types-in-constraints

Saya percaya sebanding lebih seperti tipe build in daripada antarmuka. Saya percaya ini adalah bug kecil dalam draft proposal.

type ComparableHasher interface {
    comparable
    Hash() uintptr
}

perlu menjadi

type ComparableHasher interface {
    type comparable
    Hash() uintptr
}

Taman bermain juga tampaknya menunjukkan perlu type comparable
https://go2goplay.golang.org/p/mhrl0xYsMyj

EDIT: Ian Lance Taylor dan Robert Griesemer sedang memperbaiki alat go2go (adalah bug kecil di penerjemah go2go, bukan draf. Draf desainnya benar)

Pernahkah ada pemikiran tentang memungkinkan orang untuk menulis tabel hash generik mereka sendiri dan sejenisnya? ISTM yang ada saat ini sangat terbatas (apalagi dibandingkan dengan built-in map). Pada dasarnya, peta bawaan memiliki comparable sebagai batasan kunci, tetapi tentu saja, == dan != tidak cukup untuk mengimplementasikan tabel hash. Antarmuka seperti ComparableHasher hanya meneruskan tanggung jawab untuk menulis fungsi hash ke pemanggil, itu tidak menjawab pertanyaan tentang bagaimana tampilannya sebenarnya (juga, penelepon mungkin tidak harus bertanggung jawab untuk ini; menulis fungsi hash yang baik itu sulit). Terakhir, menggunakan pointer sebagai kunci mungkin pada dasarnya tidak mungkin - mengonversi pointer ke uintptr untuk digunakan sebagai indeks akan berisiko GC memindahkan pointee dan dengan demikian ember berubah (kecuali masalah ini, memperlihatkan func hash(type T comparable)(v T) uintptr yang telah dideklarasikan sebelumnya).

Saya dapat menerima dengan baik "itu tidak benar-benar layak" sebagai jawaban, saya hanya ingin tahu apakah Anda memikirkannya :)

@gertcuykens Saya telah melakukan perbaikan pada alat go2go untuk menangani comparable sebagaimana dimaksud.

@Merovius Kami berharap bahwa orang yang menulis tabel hash generik akan menyediakan fungsi hash mereka sendiri, dan mungkin fungsi perbandingan mereka sendiri. Saat menulis fungsi hash Anda sendiri, paket https://golang.org/pkg/hash/maphash/ mungkin berguna. Anda benar bahwa hash dari nilai pointer harus bergantung pada nilai yang ditunjuk pointer tersebut; itu tidak dapat bergantung pada nilai pointer yang dikonversi ke uintptr .

Tidak yakin apakah ini batasan implementasi alat saat ini, tetapi upaya untuk mengembalikan tipe generik yang dibatasi oleh antarmuka menghasilkan kesalahan:
https://go2goplay.golang.org/p/KYRFL-vrcUF

Saya menerapkan kasus penggunaan dunia nyata yang saya miliki untuk obat generik kemarin . Ini adalah abstraksi pipa generik yang memungkinkan untuk menskalakan tahapan pipa secara independen dan mendukung pembatalan dan penanganan kesalahan (tidak berjalan di taman bermain, karena tergantung pada errgroup , tetapi menjalankannya menggunakan alat go2go tampaknya bekerja). Beberapa pengamatan:

  • Itu cukup menyenangkan. Memiliki pemeriksa tipe yang berfungsi benar-benar banyak membantu ketika mengulangi desain, dengan menerjemahkan cacat desain menjadi kesalahan tipe. Hasil akhirnya adalah ~100 LOC termasuk komentar. Jadi, secara keseluruhan, pengalaman menulis kode generik menyenangkan, IMO.
  • Kasus penggunaan ini setidaknya berfungsi dengan lancar dengan inferensi tipe, tidak diperlukan instantiasi eksplisit. Saya pikir itu menjadi pertanda baik untuk desain inferensi.
  • Saya pikir contoh ini akan mendapat manfaat dari kemampuan untuk memiliki metode dengan parameter tipe tambahan. Membutuhkan fungsi tingkat atas untuk Compose berarti konstruksi pipa terjadi secara terbalik - tahap terakhir dari pipa perlu dibangun untuk meneruskannya ke fungsi yang membangun tahap sebelumnya. Jika metode dapat memiliki parameter tipe, Anda dapat membuat Stage menjadi tipe konkret dan melakukan func (s *Stage(A, B)) Compose(type C)(n int, f func(B) C) *Stage(A, C) . Dan membangun saluran pipa akan berada dalam urutan yang sama seperti saat pemasangan pipa (lihat komentar di taman bermain). Tentu saja mungkin juga ada API yang lebih elegan dalam draf yang ada yang tidak saya lihat - sulit untuk membuktikan yang negatif. Saya akan tertarik untuk melihat contoh kerja dari itu.

Secara keseluruhan, saya suka draf baru, FWIW :) IMO menjatuhkan kontrak adalah peningkatan dan begitu juga cara baru untuk menentukan operator yang diperlukan melalui daftar tipe.

[edit: Memperbaiki bug dalam kode saya di mana kebuntuan bisa terjadi jika tahap pipa gagal. Konkurensi itu sulit]

Sebuah pertanyaan untuk cabang alat: apakah akan mengikuti rilis go terakhir (jadi v1.15, v1.15.1, ...)?

@urandom : Perhatikan bahwa nilai yang Anda kembalikan dalam kode Anda bertipe Foo(T). Setiap
instantiasi tipe seperti itu menghasilkan tipe baru yang ditentukan, dalam hal ini Foo(T).
(Tentu saja, jika Anda memiliki beberapa Foo(T) dalam kode, semuanya sama
jenis yang ditentukan).

Tetapi tipe hasil dari fungsi Anda adalah V, yang merupakan parameter tipe. Catatan
bahwa parameter tipe dibatasi oleh antarmuka Penilai, tetapi itu adalah
_bukan_ antarmuka (atau bahkan antarmuka itu). V adalah parameter tipe yang
jenis jenis baru yang kita ketahui tentang hal-hal yang dijelaskan oleh kendalanya.
Sehubungan dengan penugasan, ia bertindak seperti tipe yang ditentukan bernama V.

Jadi, Anda mencoba menetapkan nilai tipe Foo(T) ke variabel tipe V
(yang bukan Foo(T) atau Valuer(T), ia hanya memiliki sifat yang dijelaskan oleh
Penilai (T)). Dengan demikian tugas gagal.

(Selain itu, kami masih menyempurnakan pemahaman kami tentang parameter tipe
dan akhirnya perlu mengejanya dengan cukup tepat sehingga kita bisa menulis
spesifikasi Tetapi perlu diingat bahwa setiap parameter tipe secara efektif adalah yang baru
jenis yang ditentukan tentang kita hanya tahu sebanyak yang ditentukan oleh batasan jenisnya.)

Mungkin Anda bermaksud menulis ini: https://go2goplay.golang.org/p/8Hz6eWSn8Ek?

@Inuart Jika dengan cabang alat yang Anda maksud adalah cabang dev.go2go: Ini adalah prototipe, telah dibangun dengan pertimbangan kemanfaatan, dan untuk tujuan eksperimen. Kami ingin orang-orang bermain dengannya dan mencoba menulis kode, tetapi bukanlah ide yang baik untuk _bergantung_ pada penerjemah untuk perangkat lunak produksi. Banyak hal dapat berubah (bahkan sintaks, jika perlu). Kami akan memperbaiki bug dan menyesuaikan desain saat kami belajar dari umpan balik. Mengikutinya dengan rilis Go terbaru tampaknya kurang penting.

Saya menerapkan kasus penggunaan dunia nyata yang saya miliki untuk obat generik kemarin. Ini adalah abstraksi pipa umum yang memungkinkan untuk menskalakan tahapan pipa secara independen dan mendukung pembatalan dan penanganan kesalahan (tidak berjalan di taman bermain, karena tergantung pada errgroup, tetapi menjalankannya menggunakan alat go2go tampaknya berfungsi).

Saya suka contohnya. Saya baru saja membacanya sepenuhnya dan hal yang paling membuat saya tersandung (bahkan tidak layak untuk dijelaskan) tidak ada hubungannya dengan obat generik yang terlibat. Saya pikir konstruksi yang sama tanpa obat generik tidak akan lebih mudah dipahami. Ini juga pasti salah satu hal yang ingin Anda tulis sekali, dengan tes, dan tidak perlu bermain-main lagi nanti.

Satu hal yang mungkin membantu keterbacaan dan peninjauan adalah jika alat Go memiliki cara untuk menampilkan versi monomorfis dari kode generik, sehingga Anda dapat melihat bagaimana hasilnya. Mungkin tidak layak, sebagian karena fungsi bahkan mungkin tidak dimonomorfisasi dalam implementasi kompiler akhir, tetapi saya pikir itu akan berharga jika itu dapat dicapai.

Saya pikir contoh ini akan mendapat manfaat dari kemampuan untuk memiliki metode dengan parameter tipe tambahan.

Saya melihat komentar itu di taman bermain Anda juga; pasti sintaks panggilan alternatif tampaknya lebih mudah dibaca dan lugas. Bisakah Anda menjelaskan ini lebih detail? Setelah hampir tidak memikirkan kode contoh Anda, saya mengalami kesulitan melakukan lompatan :)

Jadi, Anda mencoba menetapkan nilai tipe Foo(T) ke variabel tipe V
(yang bukan Foo(T) atau Valuer(T), ia hanya memiliki sifat yang dijelaskan oleh
Penilai (T)). Dengan demikian tugas gagal.

Penjelasan yang bagus.

...Jika tidak, sedih melihat pos HN dibajak oleh kerumunan Rust. Akan menyenangkan untuk mendapatkan lebih banyak umpan balik dari Gophers tentang proposal tersebut.

Dua pertanyaan untuk tim Go:

Apakah ada perbedaan antara keduanya, atau apakah itu bug di taman bermain go2? Yang pertama mengkompilasi, yang kedua memberikan kesalahan

type Addable interface {
    type int, float64
}

func Add(type T Addable)(a, b T) T {
  return a + b
}
type Addable interface {
    type int, float64, string
}

func Add(type T Addable)(a, b T) T {
  return a + b
}

Gagal dengan: invalid operation: operator + not defined for a (variable of type T)

Nah, ini adalah kejutan yang paling tak terduga dan menyenangkan. Saya telah mengharapkan cara untuk benar-benar mencoba ini di beberapa titik, tetapi saya tidak mengharapkannya dalam waktu dekat.

Pertama-tama, menemukan bug: https://go2goplay.golang.org/p/1r0NQnJE-NZ

Kedua, saya membuat contoh iterator dan sedikit terkejut menemukan bahwa inferensi tipe itu tidak berfungsi. Saya hanya dapat memintanya mengembalikan tipe antarmuka secara langsung, tetapi saya tidak berpikir bahwa itu tidak akan dapat menyimpulkannya karena semua informasi tipe yang dibutuhkannya datang melalui argumen.

Sunting: Juga, seperti yang dikatakan banyak orang, saya pikir mengizinkan tipe baru ditambahkan selama deklarasi metode akan sangat berguna. Sejauh implementasi antarmuka berjalan, Anda bisa saja tidak mengizinkan implementasi antarmuka, hanya mengizinkan implementasi jika antarmuka juga memanggil obat generik di sana ( type Example interface { Method(type T someConstraint)(v T) bool } ), atau, mungkin, Anda dapat mengimplementasikan antarmuka jika _any_ memungkinkan variannya mengimplementasikan antarmuka, dan kemudian memanggilnya dibatasi dengan apa yang diinginkan antarmuka jika dipanggil melalui antarmuka. Sebagai contoh,

``` pergi
jenis Antarmuka antarmuka {
Dapatkan (string) string
}

tipe Contoh(tipe T) struct {
v T
}

// Ini hanya akan bekerja karena Interface.Get lebih spesifik daripada Example.Get.
func (e Contoh(T)) Dapatkan(tipe R)(v R) T {
kembalikan fmt.Sprintf("%v: %v", v, ev)
}

func Melakukan Sesuatu(antar Antarmuka) {
// Yang mendasarinya adalah Contoh(string) dan Contoh(string).Dapatkan(string) diasumsikan karena diperlukan.
fmt.Println(inter.Get("contoh"))
}

fungsi utama() {
// Diizinkan karena Contoh(string).Dapatkan(string) dimungkinkan.
Lakukan Sesuatu(Contoh(string){v: "Contoh."})
}

@DeedleFake Hal pertama yang Anda laporkan bukanlah bug. Anda harus menulis https://go2goplay.golang.org/p/qo3hnviiN4k saat ini. Ini didokumentasikan dalam draft desain. Dalam daftar parameter, penulisan a(b) ditafsirkan sebagai a (b) ( a dari tipe tanda kurung b ) untuk kompatibilitas mundur. Kami dapat mengubahnya ke depan.

Contoh Iterator menarik - sekilas memang terlihat seperti bug. Silakan ajukan bug (petunjuk dalam posting blog) dan tetapkan kepada saya. Terima kasih.

@Kashomon Posting blog (https://blog.golang.org/generics-next-step) menyarankan milis untuk diskusi dan mengajukan masalah terpisah untuk bug. Terima kasih.

Saya pikir masalah dengan + telah diperbaiki.

@toolbox

Satu hal yang mungkin membantu keterbacaan dan peninjauan adalah jika alat Go memiliki cara untuk menampilkan versi monomorfis dari kode generik, sehingga Anda dapat melihat bagaimana hasilnya. Mungkin tidak layak, sebagian karena fungsi bahkan mungkin tidak dimonomorfisasi dalam implementasi kompiler akhir, tetapi saya pikir itu akan berharga jika itu dapat dicapai.

Alat go2go dapat melakukan ini. Alih-alih menggunakan go tool go2go run x.go2 , tulis go tool go2go translate x.go2 . Itu akan menghasilkan file x.go dengan kode yang diterjemahkan.

Yang mengatakan, saya harus mengatakan bahwa itu cukup menantang untuk dibaca. Bukan tidak mungkin, tapi tidak mudah.

@griesemer

Saya mengerti bahwa argumen pengembalian bisa menjadi antarmuka, tetapi saya tidak begitu mengerti mengapa itu tidak bisa menjadi tipe generik itu sendiri.

Anda dapat, misalnya, menggunakan tipe generik yang sama sebagai parameter input, dan itu berfungsi dengan baik:
https://go2goplay.golang.org/p/LuDrlT3zLRb
Apakah ini berfungsi karena tipenya sudah dipakai?

@urandom menulis:

Saya mengerti bahwa argumen pengembalian bisa menjadi antarmuka, tetapi saya tidak begitu mengerti mengapa itu tidak bisa menjadi tipe generik itu sendiri.

Secara teoritis, Itu bisa, tetapi tidak masuk akal untuk membuat tipe pengembalian generik ketika tipe pengembalian tidak generik karena ditentukan oleh blok fungsi, yaitu oleh nilai pengembalian.

Biasanya, parameter generik sepenuhnya ditentukan oleh tupel nilai parameter atau oleh jenis aplikasi fungsi di situs panggilan (menentukan instantiasi jenis pengembalian generik).

Secara teoritis, Anda juga dapat mengizinkan parameter tipe generik yang tidak ditentukan oleh tuple nilai parameter dan harus diberikan secara eksplisit, misalnya:

func f(type S)(i int) int
{
    s S =...
    return 2
}

tidak tahu seberapa masuk akal ini.

@urandom Saya tidak bermaksud dengan contoh spesifik itu seseorang akan melewatkan sebuah kasus - ini sangat singkat. Maksud saya ketika Anda memiliki banyak metode yang hanya dapat dipanggil dengan tipe tertentu. Jadi saya berdiri dengan tidak menggunakan subtyping (seperti yang saya suka menyebutnya). Bahkan dimungkinkan untuk memecahkan "Masalah Ekspresi" tanpa menggunakan pernyataan tipe atau subtipe. Berikut caranya:

type Any interface {}

type Evaler(type t Any) interface {
  Eval() t
}

type Num struct {
  value int
}

func (n Num) Eval() int {
  return n.value
}

type Plus(type a Evaler(type t Any)) struct {
  left a
  right a
}

func (p Plus(type a Evaler(type t Any)) Eval() t {
  return p.left.Eval() + p.right.Eval()
}

func (p Plus(type a Evaler(type t Any)) String() string {
  return fmt.Sprintf("(%s+%s)", p.left, p.right)
}

type Expr interface {
  Evaler
  fmt.Stringer
}

func main() {
  var e Expr = Plus(Num){Num{1}, Num{2}}
  var v int = e.Eval() // 3
  var s string = e.String() // "(1+2)"
}

Penyalahgunaan metode Eval harus diketahui pada waktu kompilasi karena fakta bahwa itu tidak diperbolehkan untuk memanggil Eval on Plus dengan tipe yang tidak mengimplementasikan penambahan. Meskipun, dimungkinkan untuk menggunakan String() (mungkin menambahkan struct) secara tidak benar, pengujian yang baik harus menangkap kasus-kasus tersebut. Dan Go biasanya merangkul kesederhanaan di atas "kebenaran". Satu-satunya hal yang diperoleh dengan subtipe adalah lebih banyak kebingungan dalam dokumen dan penggunaan. Jika Anda dapat memberikan contoh yang memerlukan subtipe, saya mungkin lebih cenderung berpikir itu adalah ide yang bagus tetapi saat ini, saya tidak yakin.
EDIT: Memperbaiki kesalahan dan ditingkatkan

Saya tidak tahu, mengapa tidak menggunakan '<> '?

@99yun
Silakan lihat FAQ yang disertakan dengan draf yang diperbarui

Mengapa tidak menggunakan sintaks F\seperti C++ dan Java?
Saat mengurai kode dalam suatu fungsi, seperti v := F\, pada titik melihat < itu ambigu apakah kita melihat instantiasi tipe atau ekspresi menggunakan operator <. Penyelesaian yang membutuhkan pandangan ke depan yang tidak terbatas secara efektif. Secara umum kami berusaha untuk menjaga agar parser Go tetap efisien.

@urandom Badan fungsi generik selalu diperiksa tipenya tanpa instantiasi (*); secara umum (jika diekspor, misalnya) kita tidak tahu bagaimana itu akan dipakai. Saat dicentang, itu hanya bisa mengandalkan informasi yang tersedia. Jika tipe hasil adalah parameter tipe dan ekspresi pengembalian bertipe berbeda yang tidak kompatibel dengan penetapan, pengembalian tidak dapat berfungsi. Atau dengan kata lain, jika fungsi generik dipanggil dengan argumen tipe (mungkin disimpulkan), badan fungsi tidak dicentang lagi dengan argumen tipe tersebut. Itu hanya memeriksa apakah argumen tipe memenuhi batasan fungsi generik (setelah membuat instance tanda tangan fungsi dengan argumen tipe tersebut). Semoga membantu.

(*) Lebih tepatnya, fungsi generik diperiksa tipenya karena digunakan dengan parameter tipenya sendiri; parameter tipe adalah tipe nyata; kita hanya tahu tentang mereka sebanyak batasan mereka memberitahu kita.

Mari kita lanjutkan diskusi ini di tempat lain. Jika Anda memiliki lebih banyak pertanyaan dengan sepotong kode yang menurut Anda harus berfungsi, ajukan masalah agar kami dapat mendiskusikannya di sana. Terima kasih.

Sepertinya tidak ada cara untuk menggunakan fungsi untuk membuat nilai nol dari struct generik. Ambil contoh fungsi ini:

func zero(type T)() T {
    var zero T
    return zero
}

Tampaknya berfungsi untuk tipe dasar (int, float32 dll.). Namun, ketika Anda memiliki struct yang memiliki bidang generik, semuanya menjadi aneh. Ambil contoh:

type Opt(type T) struct {
    val T
}

func (o Opt(T)) Do() { /*stuff*/ }

Semua tampak baik. Namun, saat melakukan:

opt := zero(Opt(int))
opt.Do() 

itu tidak dikompilasi memberikan kesalahan: opt.Do undefined (type func() Opt(int) has no field or method Do) Saya bisa mengerti jika tidak mungkin untuk melakukan ini tapi aneh untuk berpikir itu adalah fungsi ketika int seharusnya menjadi bagian dari tipe Opt. Tetapi yang lebih aneh adalah mungkin untuk melakukan ini:

opt := zero(Opt)      //  But somehow this line compiles
opt(int).Do()         // This will panic

Saya tidak yakin bagian mana yang bug dan bagian mana yang dimaksudkan.
Kode: https://go2goplay.golang.org/p/M0VvyEYwbQU

@TotallyGamerJet

Fungsi Anda zero() tidak memiliki argumen sehingga tidak ada inferensi tipe yang terjadi. Anda harus membuat instance fungsi zero dan kemudian memanggilnya.

opt := zero(Opt(int))()
opt.Do()

https://go2goplay.golang.org/p/N6ip-nm1BP-

@toolbox
Ah iya. Saya pikir saya menyediakan tipenya tetapi saya lupa tanda kurung kedua untuk benar-benar memanggil fungsinya. Saya masih membiasakan diri dengan obat generik ini.

Saya selalu mengerti tidak memiliki obat generik di Go adalah keputusan desain bukan kelalaian. Itu telah membuat Go jauh lebih sederhana dan saya tidak dapat memahami paranoia yang berlebihan terhadap beberapa duplikasi salinan sederhana. Di perusahaan kami, kami telah membuat banyak kode Go dan tidak pernah menemukan satu pun contoh di mana kami lebih memilih obat generik.

Bagi kami itu pasti akan membuat Go merasa kurang Go dan sepertinya hype crowd akhirnya berhasil mempengaruhi perkembangan Go ke arah yang salah. Mereka tidak bisa begitu saja meninggalkan Go dalam keindahannya yang sederhana, tidak, mereka harus terus mengeluh dan mengeluh sampai akhirnya berhasil.

Maaf, ini tidak dimaksudkan untuk merendahkan siapa pun, tetapi ini adalah bagaimana penghancuran bahasa yang dirancang dengan indah dimulai. Apa berikutnya? Jika kita terus mengubah hal-hal, seperti yang diinginkan banyak orang, kita berakhir dengan "C++" atau "JavaScript".

Tinggalkan saja Go mereka seperti yang seharusnya!

@iio7 Saya IQ terendah dari semua di sini, masa depan saya bergantung pada memastikan saya dapat membaca kode orang lain. Hype baru saja dimulai bukan hanya karena generik, tetapi karena desain baru tidak memerlukan perubahan bahasa dalam proposal saat ini, jadi kami semua senang bahwa ada jendela untuk menjaga semuanya tetap sederhana dan masih memiliki beberapa barang generik dan fungsional. Jangan salah paham. Saya tahu akan selalu ada orang di tim yang menulis kode seperti ilmuwan roket dan saya si monyet mengira bisa memahaminya begitu saja? Jadi contoh yang Anda lihat sekarang adalah contoh dari ilmuwan roket dan sejujurnya, ya, saya perlu waktu untuk membacanya tetapi pada akhirnya dengan beberapa percobaan dan kesalahan saya tahu apa yang mereka coba programkan. Yang saya katakan hanyalah mempercayai Ian dan Robert dan yang lainnya, mereka belum selesai dengan desainnya. Tidak akan terkejut dalam satu tahun atau lebih ada alat yang membantu kompiler berbicara bahasa monyet sederhana yang sempurna tidak peduli seberapa sulit kode generik roket yang Anda lemparkan padanya. Umpan balik terbaik yang dapat Anda berikan adalah menulis ulang beberapa contoh dan menunjukkan jika ada sesuatu yang terlalu direkayasa sehingga mereka dapat memastikan kompiler akan mengeluh tentang hal itu atau ditulis ulang oleh sesuatu seperti alat dokter hewan secara otomatis.

Saya membaca FAQ tentang <> tetapi untuk orang bodoh seperti saya, bagaimana lebih sulit bagi pengurai untuk menentukan apakah itu panggilan umum jika terlihat seperti ini v := F<T> daripada v := F(T) ? Apakah tidak lebih sulit dengan tanda kurung karena tidak akan tahu apakah itu panggilan fungsi dengan T sebagai argumen biasa?

Selain itu, saya pikir parser tentu saja harus disimpan dengan cepat, tetapi jangan lupa juga mana yang paling mudah dibaca oleh programmer, mana yang sama pentingnya dengan IMO. Apakah lebih mudah untuk memahami apa yang dilakukan v := F(T) secara langsung? Atau v := F<T> lebih mudah? Juga penting untuk dipertimbangkan :)

Tidak berdebat untuk atau menentang v := F<T> , hanya mengemukakan beberapa pemikiran yang mungkin layak dipertimbangkan.

Ini legal. Pergi hari ini :

    f, c, d, e := 1, 2, 3, 4
    a, b := f < c, d > (e)
    fmt.Println(a, b) // true false

Tidak ada gunanya membahas kurung sudut kecuali Anda memberikan proposal tentang apa yang harus dilakukan (break back compat?). Ini adalah untuk semua maksud dan tujuan masalah mati. Secara efektif tidak ada kemungkinan kurung sudut diadopsi oleh tim Go. Silakan diskusikan hal lain.

Sunting untuk menambahkan: Maaf jika komentar ini terlalu singkat. Ada banyak diskusi tentang kurung sudut di Reddit dan HN, yang sangat membuat saya frustrasi karena masalah kompatibilitas belakang telah lama diketahui oleh orang-orang yang peduli dengan obat generik. Saya mengerti mengapa orang lebih suka kurung sudut, tetapi itu tidak mungkin tanpa perubahan yang melanggar.

Terima kasih atas komentar Anda @iio7. Selalu ada risiko bukan nol bahwa segala sesuatunya tidak terkendali. Itulah sebabnya kami sangat berhati-hati di sepanjang jalan. Saya percaya apa yang kita miliki sekarang adalah desain yang jauh lebih bersih dan lebih ortogonal daripada yang kita miliki tahun lalu; dan secara pribadi saya berharap kita dapat membuatnya lebih sederhana, terutama dalam hal mengetik daftar - tetapi kita akan mengetahuinya saat kita mempelajari lebih lanjut. (Agak ironis, semakin ortogonal dan bersih desainnya, semakin kuat dan semakin kompleks kode yang dapat ditulis.) Kata-kata terakhir belum diucapkan. Tahun lalu, ketika kami memiliki desain pertama yang berpotensi layak, reaksi banyak orang serupa dengan Anda: "Apakah kami benar-benar menginginkan ini?" Ini adalah pertanyaan yang bagus dan kita harus mencoba menjawabnya sebaik mungkin.

Pengamatan @gertcuykens juga benar - tentu saja orang-orang yang bermain dengan prototipe go2go menjelajahi batasnya sebanyak mungkin (yang kami inginkan), tetapi dalam prosesnya juga menghasilkan kode yang mungkin tidak akan berhasil dalam produksi yang tepat pengaturan. Sekarang saya telah melihat banyak kode generik yang sangat sulit untuk diuraikan.

Ada situasi di mana kode generik jelas akan menjadi kemenangan; Saya sedang memikirkan algoritme konkuren generik yang memungkinkan kita memasukkan kode yang agak halus ke dalam perpustakaan. Tentu saja ada berbagai struktur data container, dan hal-hal seperti sort, dll. Mungkin sebagian besar kode tidak memerlukan generik sama sekali. Berbeda dengan bahasa lain, di mana fitur generik merupakan pusat dari banyak hal yang dilakukan orang dalam bahasa tersebut, di Go, fitur generik hanyalah alat lain di kumpulan alat Go; bukan blok bangunan mendasar yang di atasnya segala sesuatu dibangun di atasnya.

Sebagai perbandingan: Pada hari-hari awal Go, kita semua cenderung menggunakan goroutine dan saluran secara berlebihan. Butuh beberapa saat untuk belajar kapan mereka pantas dan kapan tidak. Sekarang kami memiliki beberapa pedoman yang kurang lebih ditetapkan dan kami menggunakannya hanya jika benar-benar sesuai. Saya berharap hal yang sama akan terjadi jika kita memiliki obat generik.

Terima kasih.

Dari bagian draf desain tentang sintaks berbasis [T] :

Bahasa umumnya mengizinkan tanda koma dalam daftar yang dipisahkan koma, jadi A[T,] harus diizinkan jika A adalah tipe generik, tetapi biasanya tidak diizinkan untuk ekspresi indeks. Namun, pengurai tidak dapat mengetahui apakah A adalah tipe generik atau nilai irisan, larik, atau tipe peta, sehingga kesalahan penguraian ini tidak dapat dilaporkan hingga setelah pemeriksaan tipe selesai. Sekali lagi, dapat dipecahkan tetapi rumit.

Tidak bisakah ini diselesaikan dengan mudah hanya dengan membuat koma tambahan sepenuhnya legal dalam ekspresi indeks dan kemudian hanya dengan gofmt menghapusnya?

@DeedleFake Mungkin. Itu pasti akan menjadi jalan keluar yang mudah; tetapi juga tampak agak jelek, secara sintaksis. Saya tidak ingat semua detailnya, tetapi versi sebelumnya memiliki dukungan untuk parameter tipe gaya [type T]. Lihat cabang dev.go2go, komit 3d4810b5ba di mana dukungan telah dihapus. Seseorang bisa menggalinya lagi dan menyelidikinya.

Bisakah panjang argumen generik di setiap [] dibatasi untuk sebagian besar argumen untuk menghindari masalah ini, seperti tipe generik bawaan:

  • [N]T
  • []T
  • peta[K]T
  • chan T

Harap dicatat bahwa, argumen terakhir dalam tipe generik bawaan semuanya tidak disertakan dalam [] .
Sintaks deklarasi generik seperti: https://github.com/dotaheor/unify-Go-builtin-and-custom-generics#the -generic-declaration-syntax

@dotaheor Saya tidak yakin persis apa yang Anda tanyakan, tetapi jelas diperlukan untuk mendukung beberapa argumen tipe untuk tipe generik. Misalnya, https://go.googlesource.com/proposal/+/refs/heads/master/design/go2draft-type-parameters.md#containers .

@ianlancetaylor
Maksud saya adalah setiap parameter tipe diapit oleh [] , sehingga tipe di tautan Anda dapat dideklarasikan sebagai:

type Map[type K][type V] struct

Ketika digunakan, itu seperti:

var m Map[string]int

Argumen tipe yang tidak diapit oleh [] menunjukkan akhir dari penggunaan tipe generik.

Saat berpikir tentang pemesanan untuk array #39355 bersama dengan obat generik, saya menemukan bahwa "sebanding" ditangani khusus dalam draf obat generik saat ini (mungkin karena tidak dapat membuat daftar semua tipe yang sebanding dalam daftar tipe dengan mudah) sebagai batasan tipe yang telah dideklarasikan sebelumnya .

Akan lebih baik jika draf generik akan diubah untuk juga mendefinisikan "dipesan"/"dapat dipesan" mirip dengan bagaimana "sebanding" telah ditentukan sebelumnya. Ini adalah hubungan terkait yang umum digunakan pada nilai-nilai dari tipe yang sama dan ini akan memungkinkan ekstensi masa depan dari bahasa go untuk menentukan pemesanan pada lebih banyak tipe (array, struct, irisan, tipe jumlah, enum yang diperiksa, ...) bahwa tidak semua tipe yang dipesan akan dapat dicantumkan dalam daftar tipe seperti "sebanding".

Saya tidak menyarankan bahwa untuk diputuskan harus dipesan untuk lebih banyak jenis dalam spesifikasi bahasa tetapi perubahan ke obat generik ini membuatnya lebih maju kompatibel dengan perubahan seperti itu (batasan. Kode yang dipesan tidak harus menjadi kompiler ajaib yang dihasilkan nanti atau akan ditinggalkan jika menggunakan daftar tipe). Penyortiran paket dapat dimulai dengan batasan tipe yang telah dideklarasikan sebelumnya "dipesan" dan kemudian dapat "hanya" bekerja dengan misalnya array jika pernah diubah dan tidak ada perbaikan pada batasan yang digunakan.

@martisch Saya pikir ini hanya perlu terjadi setelah jenis yang dipesan diperpanjang. Saat ini, constraints.Ordered dapat mencantumkan semua jenis (yang tidak berfungsi untuk comparable , karena pointer, struct, array,… dapat dibandingkan, jadi itu pasti ajaib. Tapi ordered saat ini terbatas pada kumpulan terbatas tipe dasar bawaan) dan pengguna dapat mengandalkan itu. Jika kita memperluas pengurutan ke array (misalnya), kita masih dapat menambahkan batasan ordered magis baru dan menyematkannya ke constraints.Ordered . Ini berarti semua pengguna constraints.Ordered secara otomatis akan mendapat manfaat dari batasan baru. Tentu saja, pengguna yang menulis daftar tipe eksplisit mereka sendiri tidak akan mendapat manfaat - tetapi sama saja jika kita menambahkan ordered sekarang, untuk pengguna yang tidak menyematkan .

Jadi, IMO tidak ada ruginya menunda itu sampai benar-benar bermakna. Kami tidak boleh menambahkan set batasan yang mungkin sebagai pengidentifikasi yang telah dideklarasikan sebelumnya - apalagi potensi set batasan masa depan :)

Jika kita memperluas pengurutan ke array (misalnya) kita masih dapat menambahkan batasan ordered magis baru dan menyematkannya ke constraints.Ordered .

@Merovius Itu adalah poin bagus yang tidak saya pikirkan. Ini memungkinkan untuk memperpanjang constraints.Ordered di masa mendatang dengan cara yang konsisten. Jika juga akan ada constraints.Comparable maka itu cocok dengan struktur keseluruhan.

@martisch , perhatikan bahwa ordered — tidak seperti comparable — tidak koheren sebagai tipe antarmuka kecuali kita juga mendefinisikan urutan total (global) di antara tipe konkret, atau melarang kode non-generik menggunakan < pada variabel tipe ordered , atau melarang penggunaan comparable sebagai tipe antarmuka run-time umum.

Jika tidak, transitivitas "alat" rusak. Pertimbangkan fragmen program ini:

    var x constraints.Ordered = int(0)
    var y constraints.Ordered = string("0")
    fmt.Println(x < y)

Apa yang harus dikeluarkan? (Apakah jawabannya intuitif, atau arbitrer?)

@bcmils
Bagaimana dengan fun (<)(type T Ordered)(t1 T,t2 T) Bool?

Untuk membandingkan jenis aritmatika dari jenis yang berbeda:

Jika ada aritmatika S hanya mengimplementasikan Ordered(T) untuk S<:T , maka:

//Isn't possible I think
interface SorT(S,T)
{ 
type S,T
}

fun (<)(type R SorT(S,T), S Ordered(R), T Ordered(R))(s S, t T) Bool

harus unik.

Untuk polimorfisme runtime, Anda akan membutuhkan Ordered menjadi paramtrizeable.
Atau:
Anda mempartisi Diurutkan dalam tipe Tuple dan kemudian menulis ulang (<) menjadi:

//but isn't supported that either
fun(<)(type R Ordered)(s R.0,t R.1)

Hai!
Saya punya pertanyaan.

Apakah ada cara untuk membuat batasan tipe yang hanya melewati tipe generik dengan satu parameter tipe?
Sesuatu yang hanya melewati Result(T) / Option(T) /etc tetapi tidak hanya T .
Saya mencoba

type Box(type T) interface {
    Val() (T, bool)
}

tetapi membutuhkan metode Val()

type Box(type T) interface{}

mirip dengan interface{} , yaitu Any

juga mencoba https://go2goplay.golang.org/p/lkbTI7yppmh -> kompilasi gagal

type Box(type T) interface {
       type Box(T)
}

https://go2goplay.golang.org/p/5NsKWNa3E1k -> kompilasi gagal

type Box(type T) interface{}

type Generic(type T) interface {
    type Box(T)
}

https://go2goplay.golang.org/p/CKzE2J-YOpD -> tidak berfungsi

type Box(type T) interface{}

type Generic(type T Box(T)) interface {}

Apakah perilaku ini diharapkan atau hanya mengetik bug?

@tdakkota Kendala berlaku untuk argumen tipe, dan mereka berlaku untuk bentuk argumen tipe yang sepenuhnya dipakai. Tidak ada cara untuk menulis batasan tipe yang menempatkan persyaratan apa pun pada bentuk argumen tipe yang tidak dipakai.

Silakan lihat FAQ yang disertakan dengan draf yang diperbarui

Mengapa tidak menggunakan sintaks Fseperti C++ dan Java?
Saat mengurai kode dalam suatu fungsi, seperti v := F, pada titik melihat < itu ambigu apakah kita melihat instantiasi tipe atau ekspresi menggunakan operator <. Penyelesaian yang membutuhkan pandangan ke depan yang tidak terbatas secara efektif. Secara umum kami berusaha untuk menjaga agar parser Go tetap efisien.

@TotallyGamerJet Apapun!

Bagaimana menangani nilai nol dari tipe generik? Tanpa enum, bagaimana kita bisa menangani nilai opsional.
Misalnya: versi generik dari vector , dan sebuah fungsi bernama First mengembalikan elemen pertama jika panjangnya > 0 jika tidak, nilai nol dari tipe generik.
Bagaimana kita menulis kode seperti itu? Karena kita tidak tahu jenis vektor yang mana, jika chan/slice/map , kita dapat return (nil, false) , apakah struct atau primitive type like string , int , bool , bagaimana cara mengatasinya?

@leaxoy

var zero T seharusnya cukup

@leaxoy

var zero T seharusnya cukup

Variabel ajaib global seperti nil ?

@leaxoy
var zero T seharusnya cukup

Variabel ajaib global seperti nil ?

Ada proposal yang sedang dibahas untuk topik ini - lihat proposal: Go 2: universal zero value with type inference #35966 .

Ini memeriksa beberapa sintaks alternatif baru untuk ekspresi (bukan pernyataan sebagai var zero T ) yang akan selalu mengembalikan nilai nol dari suatu tipe.

Nilai nol terlihat layak saat ini, tetapi apakah mungkin mengambil ruang di tumpukan atau tumpukan? Haruskah kita mempertimbangkan untuk menggunakan enum Option untuk menyelesaikan ini dalam satu langkah.
Jika tidak, jika nilai nol tidak membutuhkan ruang, akan lebih baik dan tidak perlu menambahkan enum.

Nilai nol terlihat layak saat ini, tetapi apakah mungkin mengambil ruang di tumpukan atau tumpukan?

Secara historis, saya percaya, kompiler Go telah mengoptimalkan kasus-kasus semacam itu. Saya tidak terlalu khawatir.

Nilai tipe default dapat ditentukan dalam template C++. Apakah konstruksi serupa telah dipertimbangkan untuk parameter tipe go generik? Secara potensial ini akan memungkinkan untuk memperbaiki tipe yang ada tanpa merusak kode yang ada.

Misalnya, pertimbangkan tipe asn1.ObjectIdentifier yang ada yang merupakan []int . Satu masalah dengan tipe ini adalah tidak sesuai dengan spesifikasi ASN.1, yang menyatakan setiap sub-oid mungkin berupa INTEGER dengan panjang arbitrer (misalnya *big.Int ). Berpotensi ObjectIdentifier dapat dimodifikasi untuk menerima parameter umum, tetapi itu akan merusak banyak kode yang ada. Jika ada cara untuk menentukan int sebagai nilai parameter default, mungkin itu akan memungkinkan untuk memperbaiki kode yang ada.

type SignedInteger interface {
    type int, int32, int64, *big.Int
}
type ObjectIdentifier(type T SignedInteger) []T
// type ObjectIdentifier(type T SignedInteger=int) []T  // `int` would be the default instantiation type.

// New code with generic awareness would compile in go2.
var oid1 ObjectIdentifier(int) = ObjectIdentifier(int){1, 2, 3}

// But existing code would fail to compile:
var oid1 ObjectIdentifier = ObjectIdentifier{1, 2, 3}

Untuk lebih jelasnya, asn1.ObjectIdentifier di atas hanyalah sebuah contoh. Saya tidak mengatakan menggunakan obat generik adalah satu-satunya cara atau cara terbaik untuk menyelesaikan masalah kepatuhan ASN.1.

Selain itu, apakah ada rencana untuk memungkinkan batas antarmuka terbatas parametrizable?:

type Ordable(type T, S) interface {
    type S, type T
}

Bagaimana mendukung kondisi where pada parameter tipe.
Bisakah kita menulis kode seperti itu:

type Vector(type T) struct {
    vec []T
}

func (v Vector(T)) Sum() T where T: Summable {
      //
}

func (v Vector(T)) First()  (T, bool) {
     //
}

Metode Sum hanya berfungsi ketika parameter tipe T adalah Summable , jika tidak, kita tidak dapat memanggil Sum pada Vector.

Hai @leaxoy

Anda cukup menulis sesuatu seperti https://go2goplay.golang.org/p/pRznN30Qu8V

type Addable interface {
    type int, uint
}

type SummableVector(type T Addable) Vector(T)

func (v SummableVector(T)) Sum() T {
    var r T
    for _, i := range v.vec {
        r = r + i
    }
    return r
}

Saya pikir klausa where sepertinya tidak seperti Go dan akan sulit untuk menguraikannya, seharusnya seperti itu

type Vector(type T) struct {
    vec []T
}

func (v Vector(T Summable)) Sum() T {
      //
}

func (v Vector(T)) First()  (T, bool) {
     //
}

tapi sepertinya spesialisasi metode.

@sebastien-rosset Kami belum mempertimbangkan tipe default untuk parameter tipe generik. Bahasa tidak memiliki nilai default untuk argumen fungsi, dan tidak jelas mengapa generik akan berbeda. Menurut pendapat saya, kemampuan untuk membuat kode yang ada kompatibel dengan paket yang menambahkan obat generik bukanlah prioritas. Jika sebuah paket ditulis ulang untuk menggunakan generik, boleh saja meminta kode yang ada untuk diubah, atau cukup memperkenalkan kode generik menggunakan nama baru.

@sighoya

Selain itu, apakah ada rencana untuk memungkinkan batas antarmuka terbatas parametrizable?

Maaf, saya tidak mengerti pertanyaannya.

Saya ingin mengingatkan orang-orang bahwa posting blog (https://blog.golang.org/generics-next-step) menyarankan agar diskusi tentang obat generik dilakukan di milis golang-nuts, bukan di issue tracker. Saya akan terus membaca masalah ini, tetapi memiliki hampir 800 komentar dan benar-benar berat, selain kesulitan lain dari pelacak masalah seperti tidak memiliki threading komentar. Terima kasih.

Umpan balik: Saya mendengarkan podcast Go Time terbaru, dan saya harus mengatakan bahwa penjelasan dari @griesemer tentang masalah dengan kurung sudut adalah pertama kalinya saya benar-benar mengerti , yaitu apa arti sebenarnya dari "pandangan tak terbatas pada parser" untuk pergi? Terima kasih banyak untuk detail tambahan di sana.

Juga, saya mendukung tanda kurung siku. 😄.

@ianlancetaylor

posting blog menyarankan agar diskusi tentang obat generik dilakukan di milis kacang golang, bukan di pelacak masalah

Dalam posting blog baru-baru ini [1], @ddevault menunjukkan bahwa Grup Google (di mana milis itu berada) memerlukan akun Google. Anda memerlukan satu untuk memposting, dan tampaknya beberapa grup bahkan memerlukan akun untuk membaca. Saya memiliki akun Google, jadi ini bukan masalah bagi saya (dan saya juga tidak mengatakan saya setuju dengan semua yang ada di posting blog itu), tetapi saya setuju bahwa jika kita ingin memiliki komunitas golang yang lebih adil, dan jika kita ingin menghindari ruang gema, mungkin lebih baik tidak memiliki persyaratan semacam ini.

Saya tidak tahu ini tentang grup Google, dan jika ada pengecualian untuk kacang golang, terimalah permintaan maaf saya dan abaikan ini. Untuk apa nilainya, saya telah belajar banyak dari membaca utas ini, dan saya juga cukup yakin (setelah menggunakan golang selama lebih dari enam tahun) bahwa obat generik adalah pendekatan yang salah untuk bahasa tersebut. Hanya pendapat pribadi saya, dan terima kasih telah membawakan kami bahasa yang sangat saya sukai!

Bersulang!

[1] https://drewdevault.com/2020/08/01/pkg-go-dev-sucks.html

@purpleidea Grup Google apa pun dapat digunakan sebagai milis. Anda dapat bergabung dan berpartisipasi tanpa memiliki akun Google.

@ianlancetaylor

Grup Google apa pun dapat digunakan sebagai milis. Anda dapat bergabung dan berpartisipasi tanpa memiliki akun Google.

Ketika saya pergi ke:

https://groups.google.com/forum/#!forum/golang -nuts

di jendela browser pribadi (untuk menyembunyikan akun google saya yang saya masuki), dan klik "topik baru" itu mengarahkan saya ke halaman login google. Bagaimana cara menggunakannya tanpa akun Google?

@purpleidea Dengan menulis E-Mail ke [email protected] . Ini adalah milis. Hanya antarmuka web yang membutuhkan akun Google. Yang tampaknya adil - mengingat ini adalah milis, Anda memerlukan alamat E-Mail dan Grup jelas hanya dapat mengirim email dari akun gmail.

Saya rasa kebanyakan orang tidak mengerti apa itu milis.

Bagaimanapun, Anda juga dapat menggunakan mirror milis publik apa pun, misalnya https://www.mail-archive.com/[email protected]/

Ini semua bagus, tetapi tidak membuatnya lebih mudah ketika orang menautkan ke
utas di Google Grup (yang sering terjadi). Ini luar biasa
menjengkelkan untuk mencoba dan menemukan pesan dari ID di URL.

—Sam

Pada Minggu, 2 Agustus 2020, pukul 19:24, Ahmed W. menulis:
>
>

Saya rasa kebanyakan orang tidak mengerti apa itu milis.

Pokoknya Anda dapat menggunakan cermin milis publik apa pun juga, misalnya
https://www.mail-archive.com/[email protected]/

— Anda menerima ini karena Anda berlangganan utas ini.
Balas email ini secara langsung, lihat di GitHub
https://github.com/golang/go/issues/15292#issuecomment-667738419 , atau
berhenti berlangganan
https://github.com/notifications/unsubscribe-auth/AAD5EPNQTEUF5SPT6GMM4JLR6XYUBANCNFSM4CA35RXQ
.

--
Sam Whited

Ini bukan tempat yang tepat untuk berdiskusi.

Ada pembaruan tentang ini? 🤔.

@Imperatorn sudah ada, cuma belum dibahas disini. Diputuskan bahwa tanda kurung siku [ ] akan menjadi sintaks yang dipilih dan kata "tipe" tidak diperlukan saat menulis tipe/fungsi generik. Ada juga alias baru "apa saja" untuk antarmuka kosong.

Rancangan draf generik terbaru ada di sini .
Lihat juga komentar ini re: diskusi tentang topik ini. Terima kasih.

Saya ingin mengingatkan orang-orang bahwa posting blog (https://blog.golang.org/generics-next-step) menyarankan agar diskusi tentang obat generik dilakukan di milis golang-nuts, bukan di issue tracker. Saya akan terus membaca masalah ini, tetapi memiliki hampir 800 komentar dan benar-benar berat, selain kesulitan lain dari pelacak masalah seperti tidak memiliki threading komentar. Terima kasih.

Dalam hal ini, meskipun saya menghargai bahwa Tim Go ingin mengalihkan diskusi semacam itu dari suatu masalah karena alasan praktis, sepertinya ada banyak anggota komunitas di GitHub yang tidak menyukai golang-nut. Saya ingin tahu apakah fitur Diskusi baru GitHub akan cocok? Ternyata ada threadingnya.

@toolbox Argumen juga dapat dibuat ke arah lain - ada orang yang tidak memiliki akun github (dan menolak untuk mendapatkannya). Anda juga tidak harus berlangganan golang-nuts untuk dapat memposting dan berpartisipasi di sana.

@Merovius Salah satu fitur yang sangat saya sukai tentang masalah GitHub adalah saya dapat berlangganan notifikasi hanya untuk masalah yang saya minati. Saya tidak yakin bagaimana melakukannya dengan Google Grup?

Saya yakin ada alasan bagus untuk memilih satu atau yang lain. Pasti ada diskusi tentang forum apa yang disukai. Namun, sekali lagi, saya tidak berpikir diskusi itu harus ada di sini. Masalah ini cukup berisik.

@toolbox Argumen juga dapat dibuat ke arah lain - ada orang yang tidak memiliki akun github (dan menolak untuk mendapatkannya). Anda juga tidak harus berlangganan golang-nut untuk dapat memposting dan berpartisipasi di sana.

Saya mengerti apa yang Anda katakan, dan itu benar, tetapi Anda meleset dari sasaran. Saya tidak mengatakan bahwa pengguna golang-nuts harus diberitahu untuk pergi ke GitHub, (seperti yang terjadi sekarang secara terbalik) Saya mengatakan akan lebih baik bagi pengguna GitHub untuk memiliki forum diskusi.

Saya yakin ada alasan bagus untuk memilih satu atau yang lain. Pasti ada diskusi tentang forum apa yang disukai. Namun, sekali lagi, saya tidak berpikir diskusi itu harus ada di sini. Masalah ini cukup berisik.

Saya setuju bahwa ini sangat di luar topik untuk masalah ini, dan saya minta maaf karena telah mengungkitnya, tapi saya harap Anda melihat ironinya.

@keean @Merovius @toolbox dan teman-teman di masa depan.

FYI: Ada masalah terbuka untuk jenis diskusi ini, lihat #37469.

Halo,

Pertama-tama, terima kasih untuk Go. Bahasanya benar-benar brilian. Salah satu hal yang paling menakjubkan tentang Go, bagi saya, adalah keterbacaan. Saya baru mengenal bahasa ini, jadi saya masih dalam tahap awal penemuan, tetapi sejauh ini, bahasa itu tampak sangat jelas, tajam, dan to the point.

Sedikit umpan balik yang ingin saya sampaikan adalah bahwa dari pemindaian awal proposal obat generik, [T Constraint] tidak mudah bagi saya untuk menguraikan dengan cepat, setidaknya tidak semudah set karakter yang ditujukan untuk obat generik . Saya mengerti bahwa gaya C++ F<T Constraint> tidak layak karena sifat paradigma multi-pengembalian go. Setiap karakter non-ascii akan menjadi tugas mutlak jadi saya benar-benar berterima kasih Anda mengabaikan ide itu.

Harap pertimbangkan untuk menggunakan kombinasi karakter. Saya tidak yakin apakah operasi bitwise dapat disalahartikan atau memperkeruh air penguraian tetapi F<<T Constraint>> akan menyenangkan, menurut saya. Kombinasi simbol apa pun sudah cukup. Meskipun mungkin menambahkan beberapa pajak pemindaian mata awal, saya pikir ini dapat dengan mudah diperbaiki dengan ligatur font seperti FireCoda dan Iosevka . Tidak banyak yang dapat dilakukan untuk membedakan dengan jelas dan mudah perbedaan antara Map[T Constraint] dan map[string]T .

Saya tidak ragu bahwa orang akan melatih pikiran mereka untuk membedakan antara dua aplikasi [] berdasarkan konteksnya. Saya hanya menduga bahwa itu akan mempertajam kurva belajar.

Terima kasih untuk catatannya. Tidak ketinggalan yang jelas, tetapi map[T1]T2 dan Map[T1 Constraint] dapat dibedakan karena yang pertama tidak memiliki batasan dan yang terakhir memiliki batasan yang diperlukan.

Sintaksnya telah dibahas secara ekstensif tentang golang-nuts dan saya pikir itu sudah selesai. Kami senang mendengar komentar berdasarkan data aktual seperti penguraian ambiguitas. Untuk komentar berdasarkan perasaan dan preferensi saya pikir sudah waktunya untuk tidak setuju dan berkomitmen.

Terima kasih lagi.

@ianlancetaylor Cukup adil. Saya yakin Anda bosan mendengar nitpicks di atasnya :) Untuk apa nilainya, maksud saya dengan mudah membedakan pemindaian bijaksana.

Apapun, saya berharap untuk menggunakannya. Terima kasih.

Alternatif umum untuk reflect.MakeFunc akan menjadi kemenangan kinerja yang besar untuk instrumentasi Go. Tapi saya tidak melihat cara untuk menguraikan tipe fungsi dengan proposal saat ini.

@Julio-Guerra Saya tidak yakin apa yang Anda maksud dengan "mengurai tipe fungsi". Anda dapat, sampai taraf tertentu, membuat parameter atas argumen dan jenis pengembalian: https://go2goplay.golang.org/p/RwU11S4gC59

package main

import (
    "fmt"
)

func Call[In, Out any](f func(In) Out, v In) Out {
    return f(v)
}

func main() {
    triple := func(i int) int {
        return 3 * i
    }
    fmt.Println(Call(triple, 23))
}

Ini hanya berfungsi jika jumlah keduanya konstan.

@Julio-Guerra Saya tidak yakin apa yang Anda maksud dengan "mengurai tipe fungsi". Anda dapat, sampai taraf tertentu, membuat parameter atas argumen dan jenis pengembalian: https://go2goplay.golang.org/p/RwU11S4gC59

Memang saya mengacu pada apa yang Anda lakukan, tetapi digeneralisasikan ke parameter fungsi apa pun dan daftar tipe pengembalian (mirip dengan array parameter dan tipe pengembalian reflect.MakeFunc). Itu akan memungkinkan untuk memiliki pembungkus fungsi yang digeneralisasi (alih-alih menggunakan pembuatan kode alat).

Apakah halaman ini membantu?
0 / 5 - 0 peringkat

Masalah terkait

stub42 picture stub42  ·  3Komentar

OneOfOne picture OneOfOne  ·  3Komentar

longzhizhi picture longzhizhi  ·  3Komentar

natefinch picture natefinch  ·  3Komentar

ashb picture ashb  ·  3Komentar