Go: proposal: spesifikasi: tambahkan jenis jumlah / serikat pekerja yang didiskriminasi

Dibuat pada 6 Mar 2017  ·  320Komentar  ·  Sumber: golang/go

Ini adalah proposal untuk jenis jumlah, juga dikenal sebagai serikat pekerja yang terdiskriminasi. Jenis jumlah di Go pada dasarnya harus bertindak seperti antarmuka, kecuali bahwa:

  • mereka adalah tipe nilai, seperti struct
  • jenis yang terkandung di dalamnya diperbaiki pada waktu kompilasi

Jenis jumlah dapat dicocokkan dengan pernyataan sakelar. Kompilator memeriksa apakah semua varian cocok. Di dalam lengan pernyataan switch, nilainya dapat digunakan seolah-olah itu adalah varian yang cocok.

Go2 LanguageChange NeedsInvestigation Proposal

Komentar yang paling membantu

Terima kasih telah membuat proposal ini. Saya telah mempermainkan ide ini selama satu tahun atau lebih sekarang.
Berikut ini sejauh yang saya dapatkan dengan proposal konkret. Menurut saya
"tipe pilihan" mungkin sebenarnya nama yang lebih baik daripada "tipe jumlah", tetapi YMMV.

Jumlah jenis di Go

Jenis jumlah diwakili oleh dua atau lebih jenis yang digabungkan dengan "|"
operator.

type: type1 | type2 ...

Nilai dari tipe yang dihasilkan hanya dapat menampung salah satu dari tipe yang ditentukan. NS
tipe diperlakukan sebagai tipe antarmuka - tipe dinamisnya adalah tipe
nilai yang diberikan padanya.

Sebagai kasus khusus, "nil" dapat digunakan untuk menunjukkan apakah nilainya dapat
menjadi nihil.

Sebagai contoh:

type maybeInt nil | int

Himpunan metode dari tipe jumlah memegang persimpangan set metode
dari semua jenis komponennya, tidak termasuk metode apa pun yang memiliki kesamaan
nama tapi beda tanda tangan.

Seperti tipe antarmuka lainnya, tipe sum dapat menjadi subjek dari dinamika
konversi tipe. Pada sakelar tipe, lengan pertama sakelar yang
cocok dengan jenis yang disimpan akan dipilih.

Nilai nol dari tipe penjumlahan adalah nilai nol dari tipe pertama di
jumlah.

Saat menetapkan nilai ke tipe jumlah, jika nilainya bisa masuk lebih banyak
dari salah satu jenis yang mungkin, maka yang pertama dipilih.

Sebagai contoh:

var x int|float64 = 13

akan menghasilkan nilai dengan tipe dinamis int, tetapi

var x int|float64 = 3.13

akan menghasilkan nilai dengan tipe dinamis float64.

Penerapan

Implementasi naif dapat mengimplementasikan tipe sum persis seperti antarmuka
nilai-nilai. Pendekatan yang lebih canggih dapat menggunakan representasi
sesuai dengan himpunan nilai yang mungkin.

Misalnya tipe jumlah yang hanya terdiri dari tipe beton tanpa pointer
dapat diimplementasikan dengan tipe non-pointer, menggunakan nilai ekstra untuk
ingat jenis yang sebenarnya.

Untuk tipe sum-of-struct, bahkan dimungkinkan untuk menggunakan bantalan cadangan
byte umum untuk struct untuk tujuan itu.

Semua 320 komentar

Hal ini telah dibahas beberapa kali di masa lalu, mulai dari sebelum rilis open source. Konsensus masa lalu adalah bahwa tipe jumlah tidak banyak menambah tipe antarmuka. Setelah Anda menyelesaikan semuanya, apa yang Anda dapatkan pada akhirnya adalah tipe antarmuka di mana kompiler memeriksa apakah Anda telah mengisi semua kasus sakelar tipe. Itu manfaat yang cukup kecil untuk perubahan bahasa baru.

Jika Anda ingin mendorong proposal ini lebih jauh, Anda perlu menulis dokumen proposal yang lebih lengkap, termasuk: Apa sintaksnya? Tepatnya bagaimana mereka bekerja? (Anda mengatakan mereka adalah "tipe nilai", tetapi tipe antarmuka juga merupakan tipe nilai). Apa trade-offnya?

Saya pikir ini adalah perubahan sistem tipe yang terlalu signifikan untuk Go1 dan tidak ada kebutuhan mendesak.
Saya sarankan kita meninjau kembali ini dalam konteks yang lebih besar dari Go 2.

Terima kasih telah membuat proposal ini. Saya telah mempermainkan ide ini selama satu tahun atau lebih sekarang.
Berikut ini sejauh yang saya dapatkan dengan proposal konkret. Menurut saya
"tipe pilihan" mungkin sebenarnya nama yang lebih baik daripada "tipe jumlah", tetapi YMMV.

Jumlah jenis di Go

Jenis jumlah diwakili oleh dua atau lebih jenis yang digabungkan dengan "|"
operator.

type: type1 | type2 ...

Nilai dari tipe yang dihasilkan hanya dapat menampung salah satu dari tipe yang ditentukan. NS
tipe diperlakukan sebagai tipe antarmuka - tipe dinamisnya adalah tipe
nilai yang diberikan padanya.

Sebagai kasus khusus, "nil" dapat digunakan untuk menunjukkan apakah nilainya dapat
menjadi nihil.

Sebagai contoh:

type maybeInt nil | int

Himpunan metode dari tipe jumlah memegang persimpangan set metode
dari semua jenis komponennya, tidak termasuk metode apa pun yang memiliki kesamaan
nama tapi beda tanda tangan.

Seperti tipe antarmuka lainnya, tipe sum dapat menjadi subjek dari dinamika
konversi tipe. Pada sakelar tipe, lengan pertama sakelar yang
cocok dengan jenis yang disimpan akan dipilih.

Nilai nol dari tipe penjumlahan adalah nilai nol dari tipe pertama di
jumlah.

Saat menetapkan nilai ke tipe jumlah, jika nilainya bisa masuk lebih banyak
dari salah satu jenis yang mungkin, maka yang pertama dipilih.

Sebagai contoh:

var x int|float64 = 13

akan menghasilkan nilai dengan tipe dinamis int, tetapi

var x int|float64 = 3.13

akan menghasilkan nilai dengan tipe dinamis float64.

Penerapan

Implementasi naif dapat mengimplementasikan tipe sum persis seperti antarmuka
nilai-nilai. Pendekatan yang lebih canggih dapat menggunakan representasi
sesuai dengan himpunan nilai yang mungkin.

Misalnya tipe jumlah yang hanya terdiri dari tipe beton tanpa pointer
dapat diimplementasikan dengan tipe non-pointer, menggunakan nilai ekstra untuk
ingat jenis yang sebenarnya.

Untuk tipe sum-of-struct, bahkan dimungkinkan untuk menggunakan bantalan cadangan
byte umum untuk struct untuk tujuan itu.

@rogpeppe Bagaimana itu berinteraksi dengan pernyataan tipe dan sakelar tipe? Agaknya itu akan menjadi kesalahan waktu kompilasi untuk memiliki case pada tipe (atau pernyataan ke tipe) yang bukan anggota dari jumlah. Apakah juga merupakan kesalahan untuk memiliki sakelar yang tidak lengkap pada tipe seperti itu?

Untuk sakelar tipe, jika Anda memiliki

type T int | interface{}

dan kamu juga:

switch t := t.(type) {
  case int:
    // ...

dan t berisi antarmuka{} yang berisi int, apakah cocok dengan kasus pertama? Bagaimana jika kasus pertama adalah case interface{} ?

Atau bisakah tipe sum hanya berisi tipe konkret?

Bagaimana dengan type T interface{} | nil ? Jika Anda menulis

var t T = nil

apa itu tipe t? Atau apakah konstruksi itu dilarang? Pertanyaan serupa muncul untuk type T []int | nil , jadi ini bukan hanya tentang antarmuka.

Ya, saya pikir masuk akal untuk memiliki kesalahan waktu kompilasi
untuk memiliki kasus yang tidak dapat ditandingi. Tidak yakin apakah itu
ide bagus untuk mengizinkan sakelar yang tidak lengkap pada tipe seperti itu - kami
tidak memerlukan kelengkapan di tempat lain. Satu hal yang mungkin
baiklah: jika sakelarnya lengkap, kami tidak dapat meminta default
untuk membuatnya menjadi pernyataan penghentian.

Itu berarti Anda bisa membuat kompiler error jika Anda memiliki:

func addOne(x int|float64) int|float64 {
    switch x := x.(type) {
    case int:
        return x + 1
    case float64:
         return x + 1
    }
}

dan Anda mengubah jenis jumlah untuk menambahkan kasus tambahan.

Untuk sakelar tipe, jika Anda memiliki

ketik T int | antarmuka{}

dan kamu juga:

beralih t := t.(tipe) {
kasus int:
// ...
dan t berisi antarmuka{} yang berisi int, apakah cocok dengan kasus pertama? Bagaimana jika kasus pertama adalah antarmuka kasus{}?

t tidak boleh berisi antarmuka{} yang berisi int. t adalah antarmuka
ketik seperti jenis antarmuka lainnya, kecuali bahwa itu hanya bisa
berisi set enumerasi jenis yang terdiri dari.
Sama seperti antarmuka{} tidak bisa berisi antarmuka{} yang berisi int.

Jenis jumlah dapat cocok dengan jenis antarmuka, tetapi mereka masih mendapatkan yang konkret
ketik untuk nilai dinamis. Misalnya, akan baik untuk memiliki:

type R io.Reader | io.ReadCloser

Bagaimana dengan antarmuka tipe T{} | nol? Jika Anda menulis

var t T = nihil

apa itu tipe t? Atau apakah konstruksi itu dilarang? Pertanyaan serupa muncul untuk tipe T []int | nihil, jadi ini bukan hanya tentang antarmuka.

Menurut proposal di atas, Anda mendapatkan item pertama
dalam jumlah nilai yang dapat diberikan, jadi
Anda akan mendapatkan antarmuka nihil.

Bahkan antarmuka{} | nil secara teknis berlebihan, karena antarmuka apa pun{}
bisa nihil.

Untuk []int | nil, a nil []int tidak sama dengan antarmuka nil, jadi
nilai konkret ([]int|nil)(nil) akan menjadi []int(nil) not untyped nil .

Kasus []int | nil menarik. Saya berharap nil dalam deklarasi tipe selalu berarti "nilai antarmuka nihil", dalam hal ini

type T []int | nil
var x T = nil

akan menyiratkan bahwa x adalah antarmuka nil, bukan nil []int .

Nilai itu akan berbeda dari nil []int dikodekan dalam tipe yang sama:

var y T = []int(nil)  // y != x

Tidakkah nil selalu diperlukan bahkan jika jumlahnya adalah semua tipe nilai? Kalau tidak, apa jadinya var x int64 | float64 ? Pikiran pertama saya, mengekstrapolasi dari aturan lain, akan menjadi nilai nol dari tipe pertama, tetapi kemudian bagaimana dengan var x interface{} | int ? Itu akan, seperti yang ditunjukkan oleh @bcmils , harus menjadi jumlah nil yang berbeda.

Tampaknya terlalu halus.

Sakelar tipe lengkap akan menyenangkan. Anda selalu dapat menambahkan default: kosong jika itu bukan perilaku yang diinginkan.

Proposal mengatakan "Saat menetapkan nilai ke tipe jumlah, jika nilainya bisa masuk lebih banyak
dari salah satu jenis yang mungkin, maka yang pertama dipilih."

Jadi, dengan:

type T []int | nil
var x T = nil

x akan memiliki tipe beton []int karena nil dapat ditetapkan ke []int dan []int adalah elemen pertama dari tipe tersebut. Itu akan sama dengan nilai []int (nil) lainnya.

Tidakkah nil selalu diperlukan bahkan jika jumlahnya adalah semua tipe nilai? Kalau tidak, apa yang akan var x int64 | float64 menjadi?

Proposal mengatakan "Nilai nol dari tipe penjumlahan adalah nilai nol dari tipe pertama di
jumlah.", jadi jawabannya adalah int64(0).

Pikiran pertama saya, mengekstrapolasi dari aturan lain, adalah nilai nol dari tipe pertama, tetapi kemudian bagaimana dengan var x interface{} | int? Itu akan, seperti yang ditunjukkan oleh @bcmils , harus menjadi jumlah nil yang berbeda

Tidak, itu hanya akan menjadi nilai nil antarmuka biasa dalam kasus itu. Tipe itu (antarmuka{} | nil) berlebihan. Mungkin ide yang baik untuk membuatnya menjadi kompiler untuk menentukan tipe jumlah di mana satu elemen adalah superset dari elemen lain, karena saat ini saya tidak dapat melihat titik apa pun dalam mendefinisikan tipe seperti itu.

Nilai nol dari tipe penjumlahan adalah nilai nol dari tipe pertama dalam penjumlahan.

Itu adalah saran yang menarik, tetapi karena tipe sum harus mencatat di suatu tempat jenis nilai yang saat ini dipegangnya, saya percaya itu berarti bahwa nilai nol dari tipe sum tidak semua-byte-nol, yang akan membuatnya berbeda dari setiap jenis lainnya di Go. Atau mungkin kita bisa menambahkan pengecualian yang mengatakan bahwa jika informasi tipe tidak ada, maka nilainya adalah nilai nol dari tipe pertama yang terdaftar, tetapi kemudian saya tidak yakin bagaimana merepresentasikan nil jika tidak jenis pertama yang terdaftar.

Jadi (stuff) | nil hanya masuk akal ketika tidak ada sesuatu dalam (barang) yang bisa nihil dan nil | (stuff) berarti sesuatu yang berbeda tergantung pada apakah sesuatu dalam barang bisa nihil? Nilai apa yang ditambahkan nihil?

@ianlancetaylor Saya percaya banyak bahasa fungsional mengimplementasikan tipe jumlah (tertutup) pada dasarnya seperti yang Anda lakukan di C

struct {
    int which;
    union {
         A a;
         B b;
         C c;
    } summands;
}

jika which mengindeks ke dalam bidang gabungan secara berurutan, 0 = a, 1 = b, 2 = c, definisi nilai nol bekerja untuk semua byte adalah nol. Dan Anda perlu menyimpan jenisnya di tempat lain, tidak seperti antarmuka. Anda juga memerlukan penanganan khusus untuk tag nil dari beberapa jenis di mana pun Anda menyimpan info jenis.

Itu akan membuat tipe nilai serikat alih-alih antarmuka khusus, yang juga menarik.

Apakah ada cara untuk membuat semua nilai nol berfungsi jika bidang yang mencatat tipe memiliki nilai nol yang mewakili tipe pertama? Saya berasumsi bahwa salah satu cara yang mungkin untuk mewakili ini adalah:

type A = B|C
struct A {
  choice byte // value 0 or 1
  value ?// (thing big enough to store B | C)
}

[sunting]

Maaf @jimmyfrasche memukul saya sampai

Apakah ada sesuatu yang ditambahkan oleh nil yang tidak bisa dilakukan dengan

type S int | string | struct{}
var None struct{}

?

Sepertinya itu menghindari banyak kebingungan (yang saya miliki, setidaknya)

Atau lebih baik

type (
     None struct{}
     S int | string | None
)

dengan cara itu Anda bisa mengetikkan switch pada None dan menetapkan dengan None{}

@jimmyfrasche struct{} tidak sama dengan nil . Ini adalah detail kecil, tetapi itu akan membuat sakelar tipe pada jumlah yang tidak perlu(?) menyimpang dari sakelar tipe pada tipe lain.

@bcmils Bukan maksud saya untuk mengklaim sebaliknya — maksud saya itu dapat digunakan untuk tujuan yang sama seperti membedakan kurangnya nilai tanpa tumpang tindih dengan arti nil di salah satu jenis dalam jumlah.

@rogpeppe apa yang dicetak ini?

// r is an io.Reader interface value holding a type that also implements io.Closer
var v io.ReadCloser | io.Reader = r
switch v.(type) {
case io.ReadCloser: fmt.Println("ReadCloser")
case io.Reader: fmt.Println("Reader")
}

Saya akan menganggap "Pembaca"

@jimmyfrasche Saya akan menganggap ReadCloser , sama seperti yang Anda dapatkan dari sakelar tipe pada antarmuka lain mana pun.

(Dan saya juga mengharapkan jumlah yang hanya menyertakan tipe antarmuka untuk menggunakan tidak lebih banyak ruang daripada antarmuka biasa, meskipun saya kira tag eksplisit dapat menghemat sedikit overhead pencarian di sakelar tipe.)

@bcmils itu tugas yang menarik, pertimbangkan: https://play.golang.org/p/PzmWCYex6R

@ianlancetaylor Itu poin yang bagus untuk diangkat, terima kasih. Saya tidak berpikir sulit untuk menyiasatinya, meskipun itu menyiratkan bahwa saran "implementasi naif" saya sendiri terlalu naif. Jenis sum, meskipun diperlakukan sebagai jenis antarmuka, tidak harus benar-benar berisi penunjuk langsung ke jenis dan set metodenya - sebaliknya ia dapat, bila sesuai, berisi tag integer yang menyiratkan jenisnya. Tag itu bisa bukan nol bahkan ketika jenisnya sendiri nihil.

Diberikan:

 var x int | nil = nil

nilai runtime x tidak harus semuanya nol. Saat mengaktifkan tipe x atau mengonversi
ke tipe antarmuka lain, tag dapat diarahkan melalui tabel kecil yang berisi
pointer tipe yang sebenarnya.

Kemungkinan lain adalah mengizinkan tipe nil hanya jika itu adalah elemen pertama, tapi
yang menghalangi konstruksi seperti:

var t nil | int
var u float64 | t

@jimmyfrasche Saya akan menganggap ReadCloser, sama seperti yang Anda dapatkan dari sakelar tipe pada antarmuka lainnya.

Ya.

@bcmils itu tugas yang menarik, pertimbangkan: https://play.golang.org/p/PzmWCYex6R

Saya tidak mengerti. Mengapa "ini [...] harus valid untuk sakelar tipe untuk mencetak ReadCloser"
Seperti tipe antarmuka apa pun, tipe sum tidak akan menyimpan lebih dari nilai konkret dari apa yang ada di dalamnya.

Ketika ada beberapa jenis antarmuka dalam jumlah, representasi runtime hanyalah nilai antarmuka - hanya saja kita tahu bahwa nilai yang mendasarinya harus mengimplementasikan satu atau lebih dari kemungkinan yang dideklarasikan.

Artinya, ketika Anda menetapkan sesuatu ke tipe (I1 | I2) di mana I1 dan I2 adalah tipe antarmuka, tidak mungkin untuk mengetahui nanti apakah nilai yang Anda masukkan diketahui mengimplementasikan I1 atau I2 pada saat itu.

Jika Anda memiliki tipe itu io.ReadCloser | io.Reader Anda tidak dapat memastikan ketika Anda mengetik switch atau menegaskan pada io.Reader bahwa itu bukan io.ReadCloser kecuali jika penugasan ke tipe sum membuka kotak dan me-rebox antarmuka.

Pergi ke arah lain, jika Anda memiliki io.Reader | io.ReadCloser itu tidak akan pernah menerima io.ReadCloser karena berjalan dari kanan ke kiri atau implementasinya harus mencari antarmuka "pencocokan terbaik" dari semua antarmuka dalam jumlah tetapi itu tidak dapat didefinisikan dengan baik.

@rogpeppe Dalam proposal Anda, mengabaikan kemungkinan pengoptimalan dalam implementasi dan seluk-beluk nilai nol, manfaat utama menggunakan tipe sum di atas tipe antarmuka yang dibuat secara manual (berisi persimpangan metode yang relevan) adalah bahwa pemeriksa tipe dapat menunjukkan kesalahan pada waktu kompilasi daripada runtime. Manfaat kedua adalah nilai tipe lebih terdiskriminasi dan dengan demikian dapat membantu keterbacaan/pemahaman suatu program. Apakah ada manfaat utama lainnya?

(Saya tidak mencoba untuk mengurangi proposal dengan cara apa pun, hanya mencoba untuk mendapatkan intuisi saya dengan benar. Terutama jika kompleksitas sintaksis dan semantik ekstra "cukup kecil" - apa pun artinya - saya pasti dapat melihat manfaat memiliki kompiler menangkap kesalahan lebih awal.)

@griesemer Ya, itu benar.

Khususnya ketika mengomunikasikan pesan melalui saluran atau jaringan, saya pikir itu membantu keterbacaan dan kebenaran untuk dapat memiliki jenis yang mengungkapkan dengan tepat kemungkinan yang tersedia. Saat ini umum untuk melakukan upaya setengah hati untuk melakukan ini dengan memasukkan metode yang tidak diekspor dalam jenis antarmuka, tetapi ini a) dapat dielakkan dengan menyematkan dan b) sulit untuk melihat semua jenis yang mungkin karena metode yang tidak diekspor disembunyikan.

@jimmyfrasche

Jika Anda memiliki tipe itu io.ReadCloser | io.Reader Anda tidak dapat memastikan ketika Anda mengetik switch atau menegaskan pada io.Reader bahwa itu bukan io.ReadCloser kecuali jika penugasan ke tipe sum membuka kotak dan me-rebox antarmuka.

Jika Anda memiliki tipe itu, Anda tahu bahwa itu selalu io.Reader (atau nihil, karena io.Reader apa pun juga bisa nihil). Kedua alternatif tersebut tidak eksklusif - tipe penjumlahan seperti yang diusulkan adalah "inklusif atau" bukan "eksklusif atau".

Pergi ke arah lain, jika Anda memiliki io.Reader | io.ReadCloser itu tidak akan pernah menerima io.ReadCloser karena berjalan dari kanan ke kiri atau implementasinya harus mencari antarmuka "pencocokan terbaik" dari semua antarmuka dalam jumlah tetapi itu tidak dapat didefinisikan dengan baik.

Jika dengan "berjalan ke arah lain", maksud Anda menugaskan ke jenis itu, proposal mengatakan:

"Saat menetapkan nilai ke tipe jumlah, jika nilainya bisa masuk lebih banyak
dari salah satu jenis yang mungkin, maka yang pertama dipilih."

Dalam hal ini, io.ReadCloser dapat masuk ke dalam io.Reader dan io.ReadCloser, jadi ia memilih io.Reader, tetapi sebenarnya tidak ada cara untuk mengetahuinya setelahnya. Tidak ada perbedaan yang dapat dideteksi antara tipe io.Reader dan tipe io.Reader | io.ReadCloser, karena io.Reader juga dapat menampung semua tipe antarmuka yang mengimplementasikan io.Reader. Itu sebabnya saya curiga mungkin ide yang bagus untuk membuat kompiler menolak tipe seperti ini. Misalnya, itu bisa menolak semua tipe penjumlahan yang melibatkan antarmuka{} karena antarmuka{} sudah bisa berisi tipe apa pun, sehingga kualifikasi tambahan tidak menambahkan informasi apa pun.

@rogpeppe ada banyak hal yang saya suka tentang proposal Anda. Semantik penugasan kiri ke kanan dan nilai nol adalah nilai nol dari aturan tipe paling kiri sangat jelas dan sederhana. Sangat pergi.

Yang saya khawatirkan adalah menetapkan nilai yang sudah dikotakkan di antarmuka ke variabel jumlah yang diketik.

Mari, untuk saat ini, gunakan contoh saya sebelumnya dan katakan bahwa RC adalah struct yang dapat ditetapkan ke io.ReadCloser.

Jika kamu melakukan ini

var v io.ReadCloser | io.Reader = RC{}

hasilnya jelas dan jelas.

Namun, jika Anda melakukan ini

var r io.Reader = RC{}
var v io.ReadCloser | io.Reader = r

satu-satunya hal yang masuk akal untuk dilakukan adalah memiliki v store r sebagai io.Reader, tetapi itu berarti ketika Anda mengetik switch on v Anda tidak dapat yakin bahwa ketika Anda menekan case io.Reader bahwa Anda sebenarnya tidak memiliki io.ReadCloser. Anda harus memiliki sesuatu seperti ini:

switch v := v.(type) {
case io.ReadCloser: useReadCloser(v)
case io.Reader:
  if rc, ok := v.(io.ReadCloser); ok {
    useReadCloser(rc)
  } else {
    useReader(v)
  }
}

Sekarang, ada perasaan di mana io.ReadCloser <: io.Reader, dan Anda bisa saja melarangnya, seperti yang Anda sarankan, tapi saya pikir masalahnya lebih mendasar dan mungkin berlaku untuk semua jenis proposal untuk Go†.

Katakanlah Anda memiliki tiga antarmuka A, B, dan C, masing-masing dengan metode A(), B(), dan C(), dan sebuah struct ABC dengan ketiga metode tersebut. A, B, dan C saling lepas sehingga A | B | C dan permutasinya adalah semua tipe yang valid. Tetapi Anda masih memiliki kasus seperti

var c C = ABC{}
var v A | B | C = c

Ada banyak cara untuk mengatur ulang itu dan Anda masih tidak mendapatkan jaminan yang berarti tentang apa itu v ketika antarmuka terlibat. Setelah Anda membuka kotak jumlah yang Anda butuhkan untuk membuka kotak antarmuka jika pesanan penting.

Mungkin batasannya adalah tidak ada panggilan yang bisa menjadi antarmuka sama sekali?

Satu-satunya solusi lain yang dapat saya pikirkan adalah melarang menetapkan antarmuka ke variabel yang diketik jumlah, tetapi itu tampaknya dengan caranya sendiri lebih parah.

yang tidak melibatkan konstruktor tipe untuk tipe dalam jumlah untuk disambiguasi (seperti di Haskell di mana Anda harus mengatakan Just v untuk membangun nilai tipe Maybe)—tetapi saya tidak mendukung itu sama sekali.

@jimmyfrasche Apakah kasus penggunaan untuk unboxing yang dipesan sebenarnya penting? Itu tidak jelas bagi saya, dan untuk kasus-kasus yang penting, mudah untuk mengatasinya dengan struct kotak eksplisit:

type ReadCloser struct {  io.ReadCloser }
type Reader struct { io.Reader }

var v ReadCloser | Reader = Reader{r}

@bcmils Lebih dari itu hasilnya tidak jelas dan tidak jelas dan berarti bahwa semua jaminan yang Anda inginkan dengan tipe jumlah menguap ketika antarmuka terlibat. Saya dapat melihatnya menyebabkan semua jenis bug halus dan kesalahpahaman.

Contoh struct kotak eksplisit yang Anda berikan menunjukkan bahwa melarang antarmuka dalam tipe sum tidak membatasi kekuatan tipe sum sama sekali. Ini secara efektif membuat konstruktor tipe untuk disambiguasi yang saya sebutkan di catatan kaki. Memang ini sedikit mengganggu dan merupakan langkah ekstra, tetapi sederhana dan terasa sangat sejalan dengan filosofi Go yang membiarkan konstruksi bahasa menjadi ortogonal mungkin.

semua jaminan yang Anda inginkan dengan tipe jumlah

Itu tergantung jaminan apa yang Anda harapkan. Saya pikir Anda mengharapkan tipe penjumlahan menjadi
nilai yang ditandai secara ketat, jadi dengan jenis A|B|C apa pun, Anda tahu persis apa itu statis
ketik Anda ditugaskan untuk itu. Saya melihatnya sebagai batasan tipe pada satu nilai beton
type - batasannya adalah bahwa nilainya kompatibel dengan (setidaknya) salah satu dari A, B dan C.
Pada akhirnya itu hanya sebuah antarmuka dengan nilai masuk.

Yaitu, jika suatu nilai dapat diberikan ke tipe jumlah berdasarkan itu menjadi kompatibel dengan tugas
dengan salah satu anggota tipe jumlah, kami tidak mencatat anggota mana yang pernah
"terpilih" - kami hanya mencatat nilainya sendiri. Sama seperti ketika Anda menetapkan io.Reader
ke antarmuka{}, Anda kehilangan tipe io.Reader statis dan hanya memiliki nilainya sendiri
yang kompatibel dengan io.Reader tetapi juga dengan jenis antarmuka lain yang terjadi
untuk melaksanakan.

Dalam contoh Anda:

var c C = ABC{}
var v A | B | C = c

Pernyataan tipe v ke salah satu dari A, B dan C akan berhasil. Itu tampaknya masuk akal bagi saya.

@rogpeppe semantik itu lebih masuk akal daripada yang saya bayangkan. Saya masih belum sepenuhnya yakin bahwa antarmuka dan penjumlahan bercampur dengan baik, tetapi saya tidak lagi yakin mereka tidak. Kemajuan!

Katakanlah Anda memiliki type U I | *T mana I adalah tipe antarmuka dan *T adalah tipe yang mengimplementasikan I .

Diberikan

var i I = new(T)
var u U = i

tipe dinamis dari u adalah *T , dan di

var u U = new(T)

anda dapat mengakses *T sebagai I dengan tipe pernyataan. Apakah itu benar?

Itu berarti penugasan dari nilai antarmuka yang valid ke jumlah harus mencari jenis pencocokan pertama dalam jumlah.

Itu juga akan agak berbeda dari sesuatu seperti var v uint8 | int32 | int64 = i yang, saya bayangkan, akan selalu sesuai dengan yang mana pun dari ketiga jenis itu i bahkan jika i adalah int64 yang bisa muat dalam uint8 .

Kemajuan!

Ya!

anda dapat mengakses *T itu sebagai I dengan pernyataan tipe. Apakah itu benar?

Ya.

Itu berarti penugasan dari nilai antarmuka yang valid ke jumlah harus mencari jenis pencocokan pertama dalam jumlah.

Yup, seperti yang dikatakan proposal (tentu saja kompiler tahu secara statis mana yang harus dipilih sehingga tidak ada pencarian saat runtime).

Itu juga akan agak berbeda dari sesuatu seperti var v uint8 | int32 | int64 = i yang, saya bayangkan, akan selalu menggunakan yang mana pun dari ketiga tipe i itu bahkan jika saya adalah int64 yang bisa muat di uint8.

Ya, karena kecuali i adalah konstanta, itu hanya akan dapat ditetapkan ke salah satu alternatif tersebut.

Ya, karena kecuali i adalah konstanta, itu hanya akan dapat ditetapkan ke salah satu alternatif tersebut.

Itu tidak sepenuhnya benar, saya sadari, karena aturan yang mengizinkan penugasan tipe yang tidak disebutkan namanya ke tipe yang dinamai. Saya tidak berpikir itu membuat terlalu banyak perbedaan. Aturannya tetap sama.

Jadi tipe I | *T dari posting terakhir saya secara efektif sama dengan tipe I dan io.ReadCloser | io.Reader secara efektif adalah tipe yang sama dengan io.Reader ?

Betul sekali. Kedua tipe akan dicakup oleh aturan yang saya sarankan bahwa kompiler menolak tipe jumlah di mana satu tipe adalah antarmuka yang diimplementasikan oleh tipe lain. Aturan yang sama atau serupa dapat mencakup tipe jumlah dengan tipe duplikat seperti int|int .

Satu pemikiran: mungkin tidak intuitif bahwa int|byte tidak sama dengan byte|int , tetapi mungkin baik-baik saja dalam praktiknya.

Itu berarti penugasan dari nilai antarmuka yang valid ke jumlah harus mencari jenis pencocokan pertama dalam jumlah.

Yup, seperti yang dikatakan proposal (tentu saja kompiler tahu secara statis mana yang harus dipilih sehingga tidak ada pencarian saat runtime).

Saya tidak mengikuti ini. Cara saya membacanya (yang mungkin berbeda dari apa yang dimaksudkan) setidaknya ada dua cara untuk menangani penyatuan U dari I dan T-implements-I.

1a) pada penetapan U u = t , tag disetel ke T. Pemilihan selanjutnya menghasilkan T karena tagnya adalah T.
1b) pada penetapan U u = i (i adalah benar-benar T), tag disetel ke I. Selanjutnya pemilihan menghasilkan T karena tag adalah I tetapi pemeriksaan kedua (dilakukan karena T mengimplementasikan I dan T adalah anggota U) menemukan T.

2a) seperti 1a
2b) pada penugasan U u = i (i benar-benar T), kode yang dihasilkan memeriksa nilai (i) untuk melihat apakah itu sebenarnya T, karena T mengimplementasikan I dan T juga merupakan anggota U. Karena itu, tag disetel ke T. Kemudian seleksi langsung menghasilkan T.

Dalam hal T, V, W semua mengimplementasikan I dan U = *T | *V | *W | I , penugasan U u = i memerlukan (hingga) 3 jenis pengujian.

Antarmuka dan pointer bukanlah kasus penggunaan asli untuk tipe serikat, bukan?

Saya dapat membayangkan jenis peretasan tertentu di mana implementasi "bagus" akan melakukan sedikit benturan -- misalnya, jika Anda memiliki gabungan dari 4 atau lebih sedikit jenis penunjuk di mana semua referensi disejajarkan dengan 4 byte, simpan tag di 2 yang lebih rendah bit dari nilai. Ini pada gilirannya menyiratkan bahwa tidak baik mengambil alamat anggota serikat (bagaimanapun juga tidak, karena alamat itu dapat digunakan untuk menyimpan kembali jenis "lama" tanpa menyesuaikan tag).

Atau jika kita memiliki ruang alamat 50-ish-bit dan bersedia mengambil kebebasan dengan NaNs, kita bisa menampar bilangan bulat, pointer, dan menggandakan semuanya menjadi 64-bit union, dan kemungkinan biaya sedikit mengutak-atik.

Kedua sub-saran tersebut menjijikkan, saya yakin keduanya akan memiliki sejumlah kecil (?) pendukung fanatik.

Ini pada gilirannya menyiratkan bahwa tidak baik mengambil alamat seorang anggota serikat pekerja

Benar. Tapi saya rasa hasil dari pernyataan tipe tidak dapat dialamatkan hari ini, bukan?

pada penugasan U u = i (i benar-benar T), tag disetel ke I.

Saya pikir ini adalah intinya - tidak ada tag I.

Abaikan representasi runtime sejenak dan pertimbangkan tipe sum sebagai antarmuka. Seperti halnya antarmuka apa pun, ia memiliki tipe dinamis (tipe yang disimpan di dalamnya). "Tag" yang Anda maksud adalah tipe dinamis itu.

Seperti yang Anda sarankan (dan saya mencoba menyiratkan dalam paragraf terakhir proposal) mungkin ada cara untuk menyimpan tag tipe dengan cara yang lebih efisien daripada dengan pointer ke tipe runtime, tetapi pada akhirnya selalu hanya menyandikan dinamis type dari nilai sum-type, bukan alternatif mana yang "dipilih" saat dibuat.

Antarmuka dan pointer bukanlah kasus penggunaan asli untuk tipe serikat, bukan?

Bukan, tetapi proposal apa pun harus seortogonal mungkin sehubungan dengan fitur bahasa lain, menurut saya.

@ dr2chase pemahaman saya sejauh ini adalah, jika tipe sum menyertakan tipe antarmuka apa pun dalam definisinya, maka saat runtime implementasinya identik dengan antarmuka (berisi persimpangan set metode) tetapi invarian waktu kompilasi tentang tipe yang diizinkan masih diberlakukan.

Bahkan jika tipe sum hanya berisi tipe konkret dan diimplementasikan seperti serikat terdiskriminasi gaya-C, Anda tidak akan dapat menangani nilai dalam tipe sum karena alamat itu bisa menjadi tipe (dan ukuran) yang berbeda setelah Anda mengambil alamat. Anda bisa mengambil alamat dari nilai jumlah yang diketik itu sendiri.

Apakah diinginkan bahwa tipe jumlah berperilaku seperti ini? Kita dapat dengan mudah mendeklarasikan bahwa tipe yang dipilih/ditegaskan sama dengan apa yang dikatakan/disiratkan oleh programmer ketika suatu nilai ditetapkan ke serikat pekerja. Jika tidak, kita mungkin diarahkan ke tempat-tempat menarik sehubungan dengan int8 vs int16 vs int32, dll. Atau, misalnya, int8 | uint8 .

Apakah diinginkan bahwa tipe jumlah berperilaku seperti ini?

Itu masalah penilaian. Saya percaya itu, karena kita sudah memiliki konsep antarmuka dalam bahasa - nilai dengan tipe statis dan dinamis. Jenis jumlah seperti yang diusulkan hanya memberikan cara yang lebih tepat untuk menentukan jenis antarmuka dalam beberapa kasus. Ini juga berarti bahwa tipe sum dapat bekerja tanpa batasan pada tipe lainnya. Jika Anda tidak melakukannya, Anda perlu mengecualikan jenis antarmuka dan kemudian fitur tersebut tidak sepenuhnya ortogonal.

Jika tidak, kita mungkin akan diarahkan ke tempat-tempat menarik sehubungan dengan int8 vs int16 vs int32, dll. Atau, misalnya, int8 | uint8.

Apa yang menjadi perhatian Anda di sini?

Anda tidak dapat menggunakan tipe fungsi sebagai tipe kunci peta. Saya tidak mengatakan bahwa itu setara, hanya saja ada preseden untuk tipe yang membatasi tipe lain. Masih terbuka untuk mengizinkan antarmuka, masih belum dijual.

Program seperti apa yang dapat Anda tulis dengan tipe jumlah yang berisi antarmuka yang tidak dapat Anda tulis jika tidak?

Kontraproposal.

Tipe gabungan adalah tipe yang mencantumkan nol atau lebih tipe, ditulis

union {
  T0
  T1
  //...
  Tn
}

Semua tipe yang terdaftar (T0, T1, ..., Tn) dalam satu kesatuan harus berbeda dan tidak ada yang bisa menjadi tipe antarmuka.

Metode dapat dideklarasikan pada tipe serikat yang ditentukan (bernama) dengan aturan biasa. Tidak ada metode yang dipromosikan dari jenis yang terdaftar.

Tidak ada penyematan untuk jenis serikat pekerja. Mencantumkan satu jenis serikat pekerja di yang lain sama dengan mencantumkan jenis lain yang valid. Namun, serikat tidak dapat membuat daftar jenisnya sendiri secara rekursif, untuk alasan yang sama bahwa type S struct { S } tidak valid.

Serikat pekerja dapat disematkan dalam struct.

Nilai tipe gabungan adalah tipe dinamis, terbatas pada salah satu tipe yang terdaftar, dan nilai tipe dinamis—dikatakan sebagai nilai tersimpan. Tepat satu dari tipe yang terdaftar adalah tipe dinamis setiap saat.

Nilai nol dari gabungan kosong adalah unik. Nilai nol dari serikat yang tidak kosong adalah nilai nol dari tipe pertama yang terdaftar di serikat.

Nilai untuk tipe gabungan, U , dapat dibuat dengan U{} untuk nilai nol. Jika U memiliki satu atau lebih tipe dan v adalah nilai dari salah satu tipe yang terdaftar, T , U{v} membuat nilai gabungan yang menyimpan v dengan tipe dinamis T . Jika v adalah jenis yang tidak tercantum dalam U yang dapat ditetapkan ke lebih dari satu jenis yang terdaftar, konversi eksplisit diperlukan untuk memperjelas.

Nilai dari tipe union U dapat dikonversi ke tipe union lain V seperti pada V(U{}) jika himpunan tipe dalam U adalah subset dari set jenis dalam V . Artinya, mengabaikan pesanan, U harus memiliki semua tipe yang sama dengan V , dan U tidak dapat memiliki tipe yang tidak ada di V tetapi V dapat memiliki tipe tidak dalam U .

Penugasan antara jenis serikat didefinisikan sebagai konvertibilitas, selama paling banyak salah satu jenis serikat didefinisikan (bernama).

Nilai dari salah satu tipe yang terdaftar, T , dari tipe gabungan U dapat ditetapkan ke variabel tipe gabungan U . Ini menetapkan tipe dinamis ke T dan menyimpan nilainya. Nilai yang kompatibel dengan tugas berfungsi seperti di atas.

Jika semua tipe yang terdaftar mendukung operator kesetaraan:

  • operator kesetaraan dapat digunakan pada dua nilai dari jenis serikat yang sama. Dua nilai dari tipe gabungan tidak pernah sama jika tipe dinamisnya berbeda.
  • nilai dari gabungan tersebut dapat dibandingkan dengan nilai dari tipe mana pun yang terdaftar. Jika tipe dinamis gabungan bukan tipe operan lain, == salah dan != benar terlepas dari nilai yang disimpan. Nilai yang kompatibel dengan tugas berfungsi seperti di atas.
  • serikat pekerja dapat digunakan sebagai kunci peta

Tidak ada operator lain yang didukung pada nilai tipe gabungan.

Pernyataan tipe terhadap tipe gabungan untuk salah satu tipe terdaftarnya berlaku jika tipe yang ditegaskan adalah tipe dinamis.

Pernyataan tipe terhadap tipe gabungan untuk tipe antarmuka berlaku jika tipe dinamisnya mengimplementasikan antarmuka itu. (Khususnya, jika semua tipe yang terdaftar mengimplementasikan antarmuka ini, pernyataan selalu berlaku).

Sakelar jenis harus lengkap, termasuk semua jenis yang terdaftar, atau berisi kasing default.

Ketik pernyataan dan sakelar jenis mengembalikan salinan nilai yang disimpan.

Refleksi paket akan memerlukan cara untuk mendapatkan tipe dinamis dan nilai tersimpan dari nilai gabungan yang direfleksikan dan cara untuk mendapatkan tipe terdaftar dari tipe gabungan yang direfleksikan.

Catatan:

Sintaks union{...} dipilih sebagian untuk membedakan dari proposal tipe jumlah di utas ini, terutama untuk mempertahankan properti bagus di tata bahasa Go, dan kebetulan untuk memperkuat bahwa ini adalah gabungan yang didiskriminasi. Akibatnya, ini memungkinkan serikat yang agak aneh seperti union{} dan union{ int } . Yang pertama dalam banyak hal setara dengan struct{} (meskipun menurut definisi tipe yang berbeda) sehingga tidak menambah bahasa, selain menambahkan tipe kosong lainnya. Yang kedua mungkin lebih bermanfaat. Misalnya, type Id union { int } sangat mirip dengan type Id struct { int } kecuali bahwa versi gabungan memungkinkan penetapan langsung tanpa harus menentukan idValue.int memungkinkannya tampak lebih seperti tipe bawaan.

Konversi disambiguasi yang diperlukan ketika berhadapan dengan jenis tugas yang kompatibel agak sulit tetapi akan menangkap kesalahan jika serikat diperbarui untuk memperkenalkan ambiguitas bahwa kode hilir tidak siap.

Kurangnya embedding adalah konsekuensi dari mengizinkan metode pada serikat pekerja dan membutuhkan pencocokan lengkap dalam sakelar tipe.

Mengizinkan metode pada serikat itu sendiri daripada mengambil persimpangan metode yang valid dari jenis yang terdaftar menghindari secara tidak sengaja mendapatkan metode yang tidak diinginkan. Ketik yang menegaskan nilai yang disimpan ke antarmuka umum memungkinkan metode pembungkus yang sederhana dan eksplisit saat promosi diinginkan. Misalnya, pada tipe serikat U semua tipe yang terdaftar mengimplementasikan fmt.Stringer :

func (u U) String() string {
  return u.(fmt.Stringer).String()
}

Di utas reddit yang ditautkan, rsc berkata:

Akan aneh untuk nilai nol dari jumlah { X; Y } berbeda dari jumlah { Y; X }. Itu bukan cara kerja penjumlahan biasanya.

Saya sudah memikirkan ini, karena ini benar-benar berlaku untuk proposal apa pun.

Itu bukan bug: itu fitur.

Mempertimbangkan

type (
  Undefined = struct{}
  UndefinedOrInt union { Undefined; int }
)

vs.

type (
  Illegal = struct{}
  IntOrIllegal union { int; Illegal }
)

UndefinedOrInt mengatakan secara default itu belum ditentukan, tetapi, ketika itu, itu akan menjadi nilai int . Ini analog dengan *int yang merupakan bagaimana tipe penjumlahan (1 + int) perlu direpresentasikan di Go sekarang dan nilai nol juga analog.

IntOrIllegal , di sisi lain, mengatakan secara default ini adalah int 0, tetapi mungkin pada titik tertentu ditandai sebagai ilegal. Ini masih analog dengan *int tetapi nilai nol lebih ekspresif dari maksud, seperti menegakkan default ke new(int) .

Ini seperti mampu merangkai bidang bool dalam struct di negatif sehingga nilai nol adalah apa yang Anda inginkan sebagai default.

Kedua nilai nol dari jumlah itu berguna dan bermakna dalam hak mereka sendiri dan programmer dapat memilih yang paling sesuai untuk situasi tersebut.

Jika jumlahnya adalah enum hari dalam seminggu (setiap hari ditentukan struct{} ), mana saja yang terdaftar lebih dulu adalah hari pertama dalam seminggu, hal yang sama untuk enum iota -style.

Juga, saya tidak mengetahui bahasa apa pun dengan tipe jumlah atau serikat yang didiskriminasi/ditandai yang memiliki konsep nilai nol. C akan menjadi yang paling dekat tetapi nilai nol adalah memori yang tidak diinisialisasi — hampir tidak dapat diikuti. Java default ke nol, saya percaya, tapi itu karena semuanya adalah referensi. Semua bahasa lain yang saya tahu memiliki konstruktor tipe wajib untuk summands sehingga sebenarnya tidak ada gagasan tentang nilai nol. Apakah ada bahasa seperti itu? Apa fungsinya?

Jika perbedaan dari konsep matematika "jumlah" dan "persatuan" adalah masalahnya, kita selalu dapat menyebutnya sesuatu yang lain (misalnya "varian").

Untuk nama: Union membingungkan c/c++ puritan. Varian terutama akrab bagi programmer COBRA dan COM di mana sebagai serikat yang didiskriminasi tampaknya lebih disukai oleh bahasa fungsional. Set adalah kata kerja dan kata benda. Saya suka kata kunci _pick_. Limbo menggunakan _pick_. Ini singkat dan menggambarkan niat tipe untuk memilih dari kumpulan tipe yang terbatas.

Nama/sintaks sebagian besar tidak relevan. Pilih akan baik-baik saja.

Salah satu proposal di utas ini sesuai dengan definisi teori himpunan.

Tipe pertama yang khusus untuk nilai nol adalah tidak relevan karena tipe teoretis menjumlahkan perjalanan, sehingga urutannya tidak relevan (A + B = B + A). Proposal saya mempertahankan properti itu, tetapi jenis produk juga bolak-balik secara teori dan dianggap berbeda dalam praktik oleh sebagian besar bahasa (termasuk Go,) jadi itu mungkin tidak penting.

@jimmyfrasche

Saya pribadi percaya bahwa melarang antarmuka sebagai anggota 'pilih' adalah kelemahan yang sangat besar. Pertama, itu benar-benar akan mengalahkan salah satu kasus penggunaan besar jenis 'pilih' - memiliki kesalahan menjadi salah satu anggota. Atau Anda ingin berurusan dengan tipe pick yang memiliki io.Reader atau string, jika Anda tidak ingin memaksa pengguna untuk menggunakan StringReader sebelumnya. Tapi secara keseluruhan, antarmuka hanyalah tipe lain, dan saya percaya seharusnya tidak ada batasan tipe untuk anggota 'pilih'. Karena itu, jika tipe pick memiliki 2 anggota antarmuka, di mana yang satu sepenuhnya tertutup oleh yang lain, itu seharusnya merupakan kesalahan waktu kompilasi, seperti yang disebutkan sebelumnya.

Apa yang saya sukai dari proposal balasan Anda adalah fakta bahwa metode dapat ditentukan pada jenis pick. Saya tidak berpikir bahwa itu harus menyediakan penampang metode anggota, karena saya tidak berpikir akan ada banyak kasus di mana metode apa pun akan menjadi milik semua anggota (dan Anda tetap memiliki antarmuka untuk itu). Dan sakelar lengkap + kasing standar adalah ide yang sangat bagus.

@rogpeppe @jimmyfrasche Sesuatu yang tidak saya lihat dalam proposal Anda adalah mengapa kita harus melakukan ini. Ada kerugian yang jelas untuk menambahkan tipe baru: ini adalah konsep baru yang harus dipelajari oleh setiap orang yang bersandar pada Go. Apa keuntungan kompensasinya? Secara khusus, apa yang diberikan tipe baru kepada kita yang tidak kita dapatkan dari tipe antarmuka?

@ianlancetaylor Robert merangkumnya dengan baik di sini: https://github.com/golang/go/issues/19412#issuecomment -288608089

@ianlancetaylor
Pada akhirnya, itu membuat kode lebih mudah dibaca, dan itu adalah arahan utama dari Go. Pertimbangkan json.Token, saat ini didefinisikan sebagai antarmuka{}, namun dokumentasi menyatakan bahwa itu sebenarnya hanya salah satu dari sejumlah tipe tertentu. Jika, di sisi lain, itu ditulis sebagai

type Token Delim | bool | float64 | Number | string | nil

Pengguna akan dapat segera melihat semua kemungkinan, dan perkakas akan dapat membuat sakelar lengkap secara otomatis. Selanjutnya, kompiler akan mencegah Anda memasukkan tipe yang tidak terduga di sana juga.

Pada akhirnya, itu membuat kode lebih mudah dibaca, dan itu adalah arahan utama dari Go.

Lebih banyak fitur berarti seseorang harus tahu lebih banyak untuk memahami kodenya. Untuk seseorang yang hanya memiliki pengetahuan rata-rata tentang suatu bahasa, keterbacaannya tentu berbanding terbalik dengan jumlah fitur [baru ditambahkan].

@cznic

Lebih banyak fitur berarti seseorang harus tahu lebih banyak untuk memahami kodenya.

Tidak selalu. Jika Anda dapat mengganti "mengetahui lebih banyak tentang bahasa" untuk "mengetahui lebih banyak tentang invarian yang didokumentasikan dengan buruk atau tidak konsisten dalam kode", itu masih bisa menjadi kemenangan bersih. (Artinya, pengetahuan global dapat menggantikan kebutuhan akan pengetahuan lokal.)

Jika pemeriksaan tipe waktu kompilasi yang lebih baik memang satu-satunya manfaat, maka kita bisa mendapatkan manfaat yang sangat mirip tanpa mengubah bahasa dengan memperkenalkan komentar yang diperiksa oleh dokter hewan. Sesuatu seperti

//vet:types Delim | bool | float64 | Number | string | nil
type Token interface{}

Sekarang, kami saat ini tidak memiliki komentar dokter hewan apa pun, jadi ini bukan saran yang sepenuhnya serius. Tapi saya serius dengan ide dasarnya: jika satu-satunya keuntungan yang kita dapatkan adalah sesuatu yang dapat kita lakukan sepenuhnya dengan alat analisis statis, apakah benar-benar layak menambahkan konsep baru yang kompleks ke bahasa yang tepat?

Banyak, mungkin semua, tes yang dilakukan oleh cmd/vet dapat ditambahkan ke bahasa, dalam arti bahwa tes tersebut dapat diperiksa oleh kompiler daripada oleh alat analisis statis terpisah. Tetapi karena berbagai alasan, kami merasa berguna untuk memisahkan vet dari compiler. Mengapa konsep ini jatuh di sisi bahasa daripada sisi dokter hewan?

@ianlancetaylor memeriksa ulang komentar: https://github.com/BurntSushi/go-sumtype

@ianlancetaylor sejauh apakah perubahan itu dibenarkan, saya telah secara aktif mengabaikannya — atau lebih tepatnya mendorongnya kembali. Membicarakannya secara abstrak tidak jelas dan tidak membantu saya: semuanya terdengar seperti "hal-hal baik itu baik dan hal-hal buruk itu buruk" bagi saya. Saya ingin mendapatkan gambaran tentang jenis apa yang sebenarnya—apa batasannya, apa implikasinya, apa kelebihannya, apa kekurangannya—sehingga saya bisa melihat bagaimana itu cocok dengan bahasa (atau tidak! ) dan memiliki gagasan tentang bagaimana saya akan/dapat menggunakannya dalam program. Saya pikir saya punya ide bagus tentang apa arti tipe jumlah di Go sekarang, setidaknya dari sudut pandang saya. Saya tidak sepenuhnya yakin mereka sepadan (bahkan jika saya sangat menginginkannya), tetapi sekarang saya memiliki sesuatu yang solid untuk dianalisis dengan properti yang terdefinisi dengan baik yang dapat saya pikirkan. Saya tahu itu bukan jawaban sebenarnya, tapi setidaknya di situlah saya berada.

Jika pemeriksaan tipe waktu kompilasi yang lebih baik memang satu-satunya manfaat, maka kita bisa mendapatkan manfaat yang sangat mirip tanpa mengubah bahasa dengan memperkenalkan komentar yang diperiksa oleh dokter hewan.

Hal ini masih rentan terhadap kritik kebutuhan-untuk-belajar-hal-hal baru. Jika saya harus belajar tentang komentar dokter hewan ajaib itu untuk men-debug/memahami/menggunakan kode, itu adalah pajak mental, tidak peduli apakah kami menetapkannya ke anggaran Go-language atau anggaran teknis-bukan-the-Go-language. Jika ada, komentar ajaib lebih mahal karena saya tidak tahu bahwa saya perlu mempelajarinya ketika saya pikir saya telah mempelajari bahasa tersebut.

@cznic
saya tidak setuju. Dengan asumsi Anda saat ini, Anda tidak dapat memastikan bahwa seseorang kemudian akan memahami apa itu saluran, atau bahkan apa fungsinya. Namun hal-hal ini ada dalam bahasa. Dan fitur baru tidak secara otomatis membuat bahasa menjadi lebih sulit. Dalam hal ini, saya berpendapat bahwa itu sebenarnya akan membuatnya lebih mudah untuk dipahami, karena ini membuat pembaca segera memahami apa yang seharusnya menjadi tipe, dibandingkan dengan menggunakan tipe{} antarmuka kotak hitam.

@ianlancetaylor
Saya pribadi berpikir fitur ini lebih berkaitan dengan membuat kode lebih mudah dibaca dan dipikirkan. Keamanan waktu kompilasi adalah fitur yang sangat bagus, tetapi bukan yang utama. Tidak hanya akan membuat tanda tangan tipe menjadi lebih jelas, tetapi penggunaan selanjutnya juga akan lebih mudah dipahami, dan lebih mudah untuk ditulis. Orang tidak perlu lagi panik jika mereka menerima jenis yang tidak mereka harapkan - itu adalah perilaku saat ini bahkan di perpustakaan standar, sehingga meskipun akan lebih mudah memikirkan penggunaannya, tanpa dibebani oleh yang tidak diketahui . Dan saya tidak berpikir itu adalah ide yang baik untuk mengandalkan komentar dan alat lain (bahkan jika itu adalah pihak pertama) untuk ini, karena sintaks yang lebih bersih lebih mudah dibaca daripada komentar semacam itu. Dan komentar tidak terstruktur, dan lebih mudah untuk dikacaukan.

@ianlancetaylor

Mengapa konsep ini jatuh di sisi bahasa daripada sisi dokter hewan?

Anda dapat menerapkan pertanyaan yang sama ke fitur apa pun di luar inti turing-complete, dan bisa dibilang kami tidak ingin Go menjadi "turing tarpit". Di sisi lain, kita memiliki contoh bahasa yang telah mendorong himpunan bagian signifikan dari bahasa yang sebenarnya off menjadi generik "ekstensi" sintaks. (Misalnya, "atribut" di Rust, C++, dan GNU C.)

Alasan utama untuk menempatkan fitur dalam ekstensi atau atribut alih-alih dalam bahasa inti adalah untuk menjaga kompatibilitas sintaks, termasuk kompatibilitas dengan alat yang tidak mengetahui fitur baru. (Apakah "kompatibilitas dengan alat" benar-benar berfungsi dalam praktiknya sangat bergantung pada apa yang sebenarnya dilakukan fitur tersebut.)

Dalam konteks Go, sepertinya alasan utama untuk menempatkan fitur di vet adalah untuk mengimplementasikan perubahan yang tidak akan mempertahankan kompatibilitas Go 1 jika diterapkan pada bahasa itu sendiri. Saya tidak melihat itu sebagai masalah di sini.

Salah satu alasan untuk tidak memasukkan fitur ke dalam vet adalah jika fitur tersebut perlu disebarkan selama kompilasi. Misalnya, jika saya menulis:

switch x := somepkg.SomeFunc().(type) {
…
}

apakah saya akan mendapatkan peringatan yang tepat untuk jenis yang tidak ada dalam jumlah, melintasi batas paket? Tidak jelas bagi saya bahwa vet dapat melakukan analisis transitif sedalam itu, jadi mungkin itulah alasan mengapa ia perlu masuk ke bahasa inti.

@dr2chase Secara umum, tentu saja, Anda benar, tetapi apakah Anda benar untuk contoh khusus ini? Kode ini sepenuhnya dapat dipahami tanpa mengetahui apa arti komentar ajaib. Komentar ajaib tidak mengubah apa yang dilakukan kode dengan cara apa pun. Pesan kesalahan dari dokter hewan harus jelas.

@bcmils

Mengapa konsep ini jatuh di sisi bahasa daripada sisi dokter hewan?

Anda dapat menerapkan pertanyaan yang sama ke fitur apa pun di luar inti turing-complete....

Saya tidak setuju. Jika fitur yang dibahas mempengaruhi kode yang dikompilasi, maka ada argumen otomatis yang mendukungnya. Dalam hal ini, fitur tersebut tampaknya tidak memengaruhi kode yang dikompilasi.

(Dan, ya, dokter hewan dapat mengurai sumber paket yang diimpor.)

Saya tidak mencoba untuk mengklaim bahwa argumen saya tentang dokter hewan adalah konklusif. Tetapi setiap perubahan bahasa dimulai dari posisi negatif: bahasa yang sederhana sangat diinginkan, dan fitur baru yang signifikan seperti ini mau tidak mau membuat bahasa menjadi lebih kompleks. Anda membutuhkan argumen kuat yang mendukung perubahan bahasa. Dan dari sudut pandang saya, argumen-argumen kuat itu belum muncul. Lagi pula, kami telah memikirkan masalah ini sejak lama, dan ini adalah FAQ (https://golang.org/doc/faq#variant_types).

@ianlancetaylor

Dalam hal ini, fitur tersebut tampaknya tidak memengaruhi kode yang dikompilasi.

Saya pikir itu tergantung pada detail spesifik? Perilaku "nilai nol dari penjumlahan adalah nilai nol dari tipe pertama" yang pasti akan dilakukan oleh @jimmyfrasche di atas (https://github.com/golang/go/issues/19412#issuecomment-289319916).

@urandom Saya sedang menulis penjelasan panjang tentang mengapa antarmuka dan tipe serikat tidak bercampur tanpa konstruktor tipe eksplisit, tetapi kemudian saya menyadari ada semacam cara yang masuk akal untuk melakukan itu, jadi:

Counterproposal cepat dan kotor untuk counterproposal saya. (Apa pun yang tidak disebutkan secara eksplisit sama dengan proposal saya sebelumnya). Saya tidak yakin satu proposal lebih baik dari yang lain, tetapi yang ini memungkinkan antarmuka dan semuanya lebih eksplisit:

Serikat pekerja memiliki "nama bidang" eksplisit yang selanjutnya disebut "nama tag":

union { //or whatever
  None, invalid struct{} //None is zero value
  Good, Bad int
  Err error //okay because it's explicitly named
}

Masih belum ada penyematan. Itu selalu merupakan kesalahan untuk memiliki jenis tanpa nama tag.

Nilai gabungan memiliki tag dinamis, bukan tipe dinamis.

Penciptaan nilai literal: U{v} hanya valid jika benar-benar tidak ambigu, jika tidak maka harus U{Tag: v} .

Kompatibilitas konvertibilitas dan penetapan juga mempertimbangkan nama tag.

Penugasan ke serikat pekerja bukanlah sihir. Itu selalu berarti menetapkan nilai gabungan yang kompatibel. Untuk menyetel nilai tersimpan, nama tag yang diinginkan harus digunakan secara eksplisit: v.Good = 1 menyetel tag dinamis ke Baik dan nilai tersimpan ke 1.

Mengakses nilai yang disimpan menggunakan pernyataan tag daripada pernyataan tipe:

g := v.[Tag] //may panic
g, ok := v.[Tag] //no panic but could return zero-value, false

v.Tag adalah kesalahan pada rhs karena ambigu.

Sakelar tag seperti sakelar tipe, ditulis switch v.[type] , kecuali bahwa kasingnya adalah tag serikat pekerja.

Ketikkan pernyataan yang dipegang sehubungan dengan jenis tag dinamis. Sakelar jenis bekerja dengan cara yang sama.

Nilai yang diberikan a, b dari beberapa jenis gabungan, a == b jika tag dinamisnya sama dan nilai yang disimpan sama.

Memeriksa apakah nilai yang disimpan adalah nilai tertentu memerlukan pernyataan tag.

Jika nama tag tidak diekspor, itu hanya dapat diatur dan diakses dalam paket yang mendefinisikan serikat pekerja. Ini berarti bahwa sakelar tag dari gabungan dengan tag campuran yang diekspor dan tidak diekspor tidak akan pernah bisa lengkap di luar paket yang ditentukan tanpa kasus default. Jika semua tag tidak diekspor, itu adalah kotak hitam.

Refleksi perlu menangani nama tag juga.

e: Klarifikasi untuk serikat bersarang. Diberikan

type U union {
  A union {
    A1 T1
    A2 T2
  }
  B union {
    B1 T3
    B2 T4
  }
}
var u U

Nilai u adalah tag dinamis A dan nilai yang disimpan adalah gabungan anonim dengan tag dinamis A1 dan nilai tersimpannya adalah nilai nol dari T1.

u.B.B2 = returnsSomeT3()

hanya itu yang diperlukan untuk mengalihkan u dari nilai nol, meskipun itu berpindah dari salah satu serikat bersarang ke yang lain karena semuanya disimpan di satu lokasi memori. Tetapi

v := u.[A].[A2]

memiliki dua peluang untuk panik karena tag itu menegaskan pada dua nilai gabungan dan versi pernyataan tag yang bernilai 2 tidak tersedia tanpa membelah beberapa baris. Sakelar tag bersarang akan lebih bersih, dalam hal ini.

edit2: Klarifikasi tentang tipe menegaskan.

Diberikan

type U union {
  Exported, unexported int
}
var u U

jenis pernyataan seperti u.(int) sepenuhnya masuk akal. Dalam paket yang menentukan, itu akan selalu berlaku. Namun, jika u berada di luar paket yang ditentukan, u.(int) akan panik ketika tag dinamis adalah unexported untuk menghindari kebocoran detail implementasi. Demikian pula untuk pernyataan ke tipe antarmuka.

@ianlancetaylor Berikut adalah beberapa contoh bagaimana fitur ini akan membantu:

  1. Inti dari beberapa paket ( go/ast misalnya) adalah satu atau lebih tipe jumlah besar. Sulit untuk menavigasi paket-paket ini tanpa memahami jenis-jenis itu. Lebih membingungkan lagi, terkadang tipe penjumlahan diwakili oleh antarmuka dengan metode (misalnya go/ast.Node ), di lain waktu oleh antarmuka kosong (misalnya go/ast.Object.Decl ).

  2. Mengkompilasi fitur protobuf oneof ke Go menghasilkan tipe antarmuka yang tidak diekspor yang tujuannya hanya untuk memastikan penugasan ke salah satu bidang aman untuk tipe. Itu pada gilirannya membutuhkan menghasilkan tipe untuk setiap cabang dari salah satu. Ketik literal untuk produk akhir sulit dibaca dan ditulis:

    &sppb.Mutation{
               Operation: &sppb.Mutation_Delete_{
                   Delete: &sppb.Mutation_Delete{
                       Table:  m.table,
                       KeySet: keySetProto,
                   },
               },
    }
    

    Beberapa (meskipun tidak semua) salah satunya dapat diekspresikan dengan tipe jumlah.

  3. Terkadang tipe "mungkin" adalah yang dibutuhkan seseorang. Misalnya, banyak operasi pembaruan sumber daya Google API mengizinkan subset bidang sumber daya untuk diubah. Salah satu cara alami untuk mengekspresikan ini di Go adalah dengan varian dari struct sumber daya dengan tipe "mungkin" untuk setiap bidang. Misalnya, sumber daya ObjectAttrs Google Cloud Storage terlihat seperti

    type ObjectAttrs struct {
       ContentType string
       ...
    }
    

    Untuk mendukung pembaruan sebagian, paket juga mendefinisikan

    type ObjectAttrsToUpdate struct {
       ContentType optional.String
       ...
    }
    

    Di mana optional.String terlihat seperti ini ( godoc ):

    // String is either a string or nil.
    type String interface{}
    

    Ini sulit untuk dijelaskan dan tidak aman untuk diketik, tetapi ternyata praktis dalam praktiknya, karena literal ObjectAttrsToUpdate terlihat persis seperti literal ObjectAttrs , saat menyandikan kehadiran. Saya berharap kita bisa menulis

    type ObjectAttrsToUpdate struct {
       ContentType string | nil
       ...
    }
    
  4. Banyak fungsi mengembalikan (T, error) dengan semantik xor (T berarti jika kesalahan nol). Menulis tipe pengembalian sebagai T | error akan memperjelas semantik, meningkatkan keamanan, dan memberikan lebih banyak peluang untuk komposisi. Bahkan jika kita tidak dapat (karena alasan kompatibilitas) atau tidak ingin mengubah nilai kembalian suatu fungsi, tipe jumlah tetap berguna untuk membawa nilai tersebut, seperti menulisnya ke saluran.

Anotasi go vet diakui akan membantu banyak kasus ini, tetapi bukan kasus di mana tipe anonim masuk akal. Saya pikir jika kami memiliki tipe jumlah, kami akan melihat banyak

chan *Response | error

Jenis itu cukup pendek untuk ditulis berkali-kali.

@ianlancetaylor ini mungkin bukan awal yang baik, tetapi inilah semua yang dapat Anda lakukan dengan serikat pekerja yang sudah dapat Anda lakukan di Go1, karena saya pikir itu adil untuk mengakui dan meringkas argumen tersebut:

(Menggunakan proposal terbaru saya dengan tag untuk sintaks/semantik di bawah ini. Juga dengan asumsi kode yang dipancarkan pada dasarnya seperti kode C yang saya posting jauh sebelumnya di utas.)

Jenis jumlah tumpang tindih dengan iota, pointer, dan antarmuka.

sedikit pun

Kedua jenis ini kira-kira setara:

type Stoplight union {
  Green, Yellow, Red struct {}
}

func (s Stoplight) String() string {
  switch s.[type] {
  case Green: return "green" //etc
  }
}

dan

type Stoplight int

const (
  Green Stoplight = iota
  Yellow
  Red
)

func (s Stoplight) String() string {
  switch s {
  case Green: return "green" //etc
  }
}

Kompiler kemungkinan akan memancarkan kode yang persis sama untuk keduanya.

Dalam versi gabungan, int diubah menjadi detail implementasi tersembunyi. Dengan versi iota, Anda dapat menanyakan apa itu Kuning/Merah atau menetapkan nilai Stoplight ke -42, tetapi tidak dengan versi gabungan—itu semua adalah kesalahan kompilator dan invarian yang dapat diperhitungkan selama pengoptimalan. Demikian pula, Anda dapat menulis sakelar (nilai) yang gagal memperhitungkan lampu Kuning tetapi dengan sakelar tag, Anda memerlukan kasing default untuk membuatnya eksplisit.

Tentu saja, ada hal-hal yang dapat Anda lakukan dengan sedikit yang tidak dapat Anda lakukan dengan tipe serikat pekerja.

petunjuk

Kedua jenis ini kira-kira setara

type MaybeInt64 union {
  None struct{}
  Int64 int64
}

dan

type MaybeInt64 *int64

Versi pointer lebih ringkas. Versi serikat akan membutuhkan sedikit tambahan (yang pada gilirannya kemungkinan akan berukuran kata) untuk menyimpan tag dinamis, sehingga ukuran nilainya kemungkinan akan sama dengan https://golang.org/pkg/database/sql/ #NullInt64

Versi serikat lebih jelas mendokumentasikan maksud.

Tentu saja, ada beberapa hal yang dapat Anda lakukan dengan pointer yang tidak dapat Anda lakukan dengan tipe union.

antarmuka

Kedua jenis ini kira-kira setara

type AB union {
  A A
  B B
}

dan

type AB interface {
  secret()
}
func (A) secret() {}
func (B) secret() {}

Versi gabungan tidak dapat dielakkan dengan penyematan. A dan B tidak memerlukan metode yang sama—mereka sebenarnya bisa menjadi tipe primitif atau memiliki kumpulan metode yang sepenuhnya terpisah, seperti contoh json.Token @urandom yang diposting.

Sangat mudah untuk melihat apa yang dapat Anda masukkan ke dalam gabungan AB versus antarmuka AB: definisinya adalah dokumentasi (saya harus membaca sumber go/ast beberapa kali untuk mencari tahu apa itu).

AB union tidak akan pernah nol dan dapat diberikan metode di luar persimpangan konstituennya (ini dapat disimulasikan dengan menyematkan antarmuka dalam struct tetapi kemudian konstruksi menjadi lebih rumit dan rawan kesalahan).

Tentu saja, ada beberapa hal yang dapat Anda lakukan dengan antarmuka yang tidak dapat Anda lakukan dengan tipe serikat pekerja.

Ringkasan

Mungkin tumpang tindih itu terlalu banyak tumpang tindih.

Dalam setiap kasus, manfaat utama dari versi serikat memang adalah pemeriksaan waktu kompilasi yang lebih ketat. Apa yang tidak bisa Anda lakukan lebih penting daripada apa yang Anda bisa. Untuk kompiler yang diterjemahkan ke dalam invarian yang lebih kuat dapat digunakan untuk mengoptimalkan kode. Untuk programmer yang menerjemahkan ke dalam hal lain, Anda dapat membiarkan kompiler khawatir—itu hanya akan memberi tahu Anda jika Anda salah. Dalam versi antarmuka, setidaknya, ada manfaat dokumentasi yang penting.

Versi kikuk dari contoh iota dan pointer dapat dibangun menggunakan strategi "antarmuka dengan metode yang tidak diekspor". Namun, dalam hal ini, struct dapat disimulasikan dengan antarmuka map[string]interface{} dan (tidak kosong) dengan tipe func dan nilai metode. Tidak ada yang mau karena lebih sulit dan kurang aman.

Semua fitur itu menambahkan sesuatu ke bahasa tetapi ketidakhadirannya dapat diatasi (dengan menyakitkan, dan di bawah protes).

Jadi saya berasumsi bilahnya bukan untuk mendemonstrasikan program yang bahkan tidak dapat didekati di Go, melainkan untuk mendemonstrasikan program yang jauh lebih mudah dan ditulis dengan rapi di Go dengan serikat pekerja daripada tanpa serikat pekerja. Jadi apa yang masih harus ditunjukkan adalah itu.

@jimmyfrasche

Saya tidak melihat alasan mengapa tipe serikat pekerja harus menamai bidang. Nama hanya berguna jika Anda ingin membedakan antara bidang yang berbeda dari jenis yang sama. Namun, serikat pekerja tidak boleh memiliki banyak bidang dengan jenis yang sama, karena itu tidak ada artinya. Jadi, memiliki nama hanya berlebihan, dan menyebabkan kebingungan dan lebih banyak mengetik.

Intinya, tipe serikat pekerja Anda akan terlihat seperti:

union {
    struct{}
    int
    err
}

Tipe itu sendiri akan memberikan pengenal unik yang dapat digunakan untuk menetapkan ke serikat, sangat mirip dengan cara tipe yang disematkan dalam struct digunakan sebagai pengidentifikasi.

Namun, agar penugasan eksplisit berfungsi, seseorang tidak dapat membuat tipe gabungan dengan menetapkan tipe tanpa nama sebagai anggota, karena sintaks akan mengizinkan ekspresi seperti itu. Misal v.struct{} = struct{}

Jadi, tipe seperti raw struct, union dan funcs harus diberi nama terlebih dahulu untuk menjadi bagian dari union, dan menjadi dapat dialihkan. Dengan pemikiran ini, serikat bersarang tidak akan menjadi sesuatu yang istimewa, karena serikat dalam hanya akan menjadi tipe anggota lain.

Sekarang, saya tidak yakin sintaks mana yang lebih baik.

[union|sum|pick|oneof] {
    type1
    package1.type2
    ....
}

Di atas tampaknya lebih cocok, tetapi agak bertele-tele untuk tipe seperti itu.

Di sisi lain, type1 | package1.type2 mungkin tidak terlihat seperti tipe go Anda yang biasa, namun ia memperoleh manfaat dari penggunaan '|' simbol, yang sebagian besar dikenali sebagai OR. Dan itu mengurangi verbositas tanpa menjadi samar.

@urandom jika Anda tidak memiliki "nama tag" tetapi izinkan antarmuka, jumlahnya diciutkan menjadi interface{} dengan pemeriksaan tambahan. Mereka berhenti menjadi tipe penjumlahan karena Anda dapat memasukkan satu hal tetapi mengeluarkannya dengan berbagai cara. Nama tag membiarkan mereka menjadi tipe penjumlahan dan menahan antarmuka tanpa ambiguitas.

Namun, nama tag memperbaiki lebih dari sekadar masalah{} antarmuka. Mereka membuat tipenya jauh lebih tidak ajaib dan membiarkan semuanya menjadi sangat eksplisit tanpa harus menciptakan banyak tipe hanya untuk membedakan. Anda dapat memiliki tugas eksplisit dan mengetik literal, seperti yang Anda tunjukkan.

Bahwa Anda dapat memberikan satu jenis lebih dari satu tag adalah fitur. Pertimbangkan jenis untuk mengukur berapa banyak keberhasilan atau kegagalan yang terjadi berturut-turut (1 keberhasilan membatalkan N kegagalan dan sebaliknya)

type Counter union {
  Successes, Failures uint 
}

tanpa nama tag yang Anda perlukan

type (
  Success uint
  Failures uint
  Counter Successes | Failures
)

dan penugasan akan terlihat seperti c = Successes(1) bukannya c.Successes = 1 . Anda tidak mendapatkan banyak.

Contoh lain adalah jenis yang mewakili kegagalan lokal atau jarak jauh. Dengan nama tag, ini mudah untuk dimodelkan:

type Failure union {
  Local, Remote error
}

Penyediaan kesalahan dapat ditentukan dengan nama tagnya, terlepas dari apa kesalahan sebenarnya. Tanpa nama tag, Anda memerlukan type Local { error } dan hal yang sama untuk jarak jauh, bahkan jika Anda mengizinkan antarmuka secara langsung dalam penjumlahan.

Nama tag adalah semacam pembuatan khusus bukan alias atau tipe bernama secara lokal di serikat pekerja. Memiliki beberapa "tag" dengan tipe yang sama tidak unik untuk proposal saya: itulah yang dilakukan setiap bahasa fungsional (yang saya tahu).

Kemampuan untuk membuat tag yang tidak diekspor untuk jenis yang diekspor dan sebaliknya juga merupakan hal yang menarik.

Juga memiliki pernyataan tag dan jenis yang terpisah memungkinkan beberapa kode menarik, seperti dapat mempromosikan metode bersama ke serikat pekerja dengan pembungkus satu baris.

Sepertinya itu memecahkan lebih banyak masalah daripada yang ditimbulkannya dan membuat semuanya menjadi lebih baik. Sejujurnya saya tidak begitu yakin ketika saya menulisnya, tetapi saya menjadi semakin yakin bahwa itu satu-satunya cara untuk menyelesaikan semua masalah dengan mengintegrasikan jumlah ke dalam Go.

Untuk memperluasnya, contoh motivasi bagi saya adalah dari @rogpeppe io.Reader | io.ReadCloser . Mengizinkan antarmuka tanpa tag, ini adalah tipe yang sama dengan io.Reader .

Anda dapat memasukkan ReadCloser dan menariknya keluar sebagai Reader. Anda kehilangan A | B berarti A atau B properti dari jenis jumlah.

Jika Anda perlu spesifik tentang kadang-kadang menangani io.ReadCloser sebagai io.Reader Anda perlu membuat struct pembungkus seperti yang ditunjukkan oleh @bcmills , type Reader struct { io.Reader } dll. dan memiliki tipe menjadi Reader | ReadCloser .

Bahkan jika Anda membatasi jumlah antarmuka dengan kumpulan metode terputus-putus, Anda masih memiliki masalah ini karena satu jenis dapat mengimplementasikan lebih dari satu antarmuka tersebut. Anda kehilangan kejelasan jenis penjumlahan: mereka bukan "A atau B": mereka adalah "A atau B atau terkadang apa pun yang Anda suka".

Lebih buruk lagi, jika tipe tersebut berasal dari paket lain, mereka dapat tiba-tiba berperilaku berbeda setelah pembaruan bahkan jika Anda sangat berhati-hati untuk membangun program Anda sehingga A tidak pernah diperlakukan sama dengan B.

Awalnya saya menjelajahi antarmuka yang tidak diizinkan untuk menyelesaikan masalah. Tidak ada yang senang dengan itu! Tetapi itu juga tidak menghilangkan masalah seperti a = b berarti hal yang berbeda tergantung pada jenis a dan b, yang saya tidak nyaman. Juga harus ada banyak aturan tentang jenis apa yang dipilih dalam pemilihan ketika penetapan jenis ikut bermain. Ini banyak sihir.

Anda menambahkan tag dan semuanya hilang.

Dengan union { R io.Reader | RC io.ReadCloser } Anda dapat secara eksplisit mengatakan saya ingin ReadCloser ini dianggap sebagai Pembaca jika itu yang masuk akal. Tidak diperlukan jenis pembungkus. Ini tersirat dalam definisi. Terlepas dari jenis tag, itu salah satu tag atau yang lain.

Kelemahannya adalah, jika Anda mendapatkan io.Reader dari tempat lain, katakan chan menerima atau panggilan fungsi, dan itu mungkin io.ReadCloser dan Anda perlu menetapkannya ke tag yang tepat, Anda harus mengetikkan assert di io. Baca Lebih Dekat dan uji. Tapi itu membuat maksud dari program menjadi lebih jelas—tepatnya apa yang Anda maksud ada di dalam kode.

Juga karena pernyataan tag berbeda dari pernyataan tipe, jika Anda benar-benar tidak peduli dan hanya menginginkan io.Reader, Anda dapat menggunakan pernyataan tipe untuk menariknya, terlepas dari tag.

Ini adalah transliterasi upaya terbaik dari contoh mainan ke dalam Go tanpa serikat pekerja/jumlah/dll. Ini mungkin bukan contoh terbaik, tetapi ini adalah contoh yang saya gunakan untuk melihat seperti apa bentuknya.

Ini menunjukkan semantik dengan cara yang lebih operasional, yang kemungkinan akan lebih mudah dipahami daripada beberapa poin singkat dalam proposal.

Ada sedikit boilerplate dalam transliterasi jadi saya biasanya hanya menulis contoh pertama dari beberapa metode dengan catatan tentang pengulangan.

In Go dengan proposal serikat:

type fail union { //zero value: (Local, nil)
  Local, Remote error
}

func (f fail) Error() string {
  //Could panic if local/remote nil, but assuming
  //it will be constructed purposefully
  return f.(error).Error()
}

type U union { //zero value: (A, "")
  A, B, C string
  D, E    int
  F       fail
}

//in a different package

func create() pkg.U {
  return pkg.U{D: 7}
}

func process(u pkg.U) {
  switch u := u.[type] {
  case A:
    handleA(u) //undefined here, just doing something with unboxed value
  case B:
    handleB(u)
  case C:
    handleC(u)
  case D:
    handleD(u)
  case E:
    handleE(u)
  case F:
    switch u := u.[type] {
    case Local:
      log.Fatal(u)
    case Remote:
      log.Printf("remote error %s", u)
      retry()
    } 
  }
}

Ditransliterasikan ke Go saat ini:

(catatan disertakan tentang perbedaan antara transliterasi dan yang di atas)

const ( //simulates tags, namespaced so other packages can see them without overlap
  Fail_Local = iota
  Fail_Remote
)

//since there are only two tags with a single type this can
//be represented precisely and safely
//the error method on the full version of fail can be
//put more succinctly with type embedding in this case

type fail struct { //zero value (Fail_Local, nil) :)
  remote bool
  error
}

// e, ok := f.[Local]
func (f *fail) TagAssertLocal2() (error, bool) { //same for TagAssertRemote2
  if !f.remote {
    return nil, false
  }
  return f.error, true
}

// e := f.[Local]
func (f *fail) TagAssertLocal() error { //same for TagAssertRemote
  if !f.remote {
    panic("invalid tag assert")
  }
  return f.error
}

// f.Local = err
func (f *fail) SetLocal(err error) { //same for SetRemote
  f.remote = false
  f.error = err
}

// simulate tag switch
func (f *fail) TagSwitch() int {
  if f.remote {
    return Fail_Remote
  }
  return Fail_Local
}

// f.(someType) needs to be written as f.TypeAssert().(someType)
func (f *fail) TypeAssert() interface{} {
  return f.error
}

const (
  U_A = iota
  U_B
  // ...
  U_F
)

type U struct { //zero value (U_A, "", 0, fail{}) :(
  kind int //more than two types, need an int
  s string //these would all occupy the same space
  i int
  f fail
}

//s, ok := u.[A]
func (u *U) TagAssertA2() (string, bool) { //similar for B, etc.
  if u.kind == U_A {
    return u.s, true
  }
  return "", false
}

//s := u.[A]
func (u *U) TagAssertA() string { //similar for B, etc.
  if u.kind != U_A {
    panic("invalid tag assert")
  }
  return u.s
}

// u.A = s
func (u *U) SetA(s string) { //similar for B, etc.
  //if there were any pointers or reference types
  //in the union, they'd have to be nil'd out here,
  //since the space isn't shared
  u.kind = U_A
  u.s = s
}

// special case of u.F.Local = err
func (u *U) SetF_Local(err error) { //same for SetF_Remote
  u.kind = U_F
  u.f.SetLocal(err)
}

func (u *U) TagSwitch() int {
  return u.kind
}

func (u *U) TypeAssert() interface{} {
  switch u.kind {
  case U_A, U_B, U_C:
    return u.s
  case U_D, U_E:
    return u.i
  }
  return u.f
}

//in a different package

func create() pkg.U {
  var u pkg.U
  u.SetD(7)
  return u
}

func process(u pkg.U) {
  switch u.TagSwitch() {
  case U_A:
    handleA(u.TagAssertA())
  case U_B:
    handleB(u.TagAssertB())
  case U_C:
    handleC(u.TagAssertC())
  case U_D:
    handleD(u.TagAssertD())
  case U_E:
    handleE(u.TagAssertE())
  case U_F:
    switch u := u.TagAssertF(); u.TagSwitch() {
    case Fail_Local:
      log.Fatal(u.TagAssertLocal())
    case Fail_Remote:
      log.Printf("remote error %s", u.TagAssertRemote())
    }
  }
}

@jimmyfrasche

Karena gabungan berisi tag yang mungkin memiliki jenis yang sama, bukankah sintaks berikut akan lebih cocok:

func process(u pkg.U) {
  switch v := u {
  case A:
    handleA(v) //undefined here, just doing something with unboxed value
  case B:
    handleB(v)
  case C:
    handleC(v)
  case D:
    handleD(v)
  case E:
    handleE(v)
  case F:
    switch w := v {
    case Local:
      log.Fatal(w)
    case Remote:
      log.Printf("remote error %s", w)
      retry()
    } 
  }
}

Cara saya melihatnya, ketika digunakan dengan sakelar, serikat pekerja sangat mirip dengan tipe seperti int, atau string. Perbedaan utama adalah bahwa hanya ada 'nilai' terbatas yang dapat diberikan padanya, berbeda dengan tipe sebelumnya, dan sakelar itu sendiri sudah lengkap. Jadi, dalam hal ini saya tidak benar-benar melihat perlunya sintaks khusus, mengurangi kerja mental pengembang.

Juga, di bawah proposal ini, apakah kode tersebut valid:

type Foo union {
    // Completely different types, no ambiguity
    A string
    B int
}

func Bar(f Foo) {
    switch v := f {
        ....
    }
}

....

func main() {
    // No need for Bar(Foo{A: "hello world"})
    Bar("hello world")
    Bar(1)
}

@urandom Saya memilih sintaks untuk mencerminkan semantik menggunakan analogi dengan sintaks Go yang ada bila memungkinkan.

Dengan tipe antarmuka yang dapat Anda lakukan

var i someInterface = someValue //where someValue implements someInterface.
var j someInterface = i //this assignment is different from the last one.

Itu bagus dan tidak ambigu karena tidak masalah apa jenis someValue selama kontraknya terpenuhi.

Saat Anda memperkenalkan tag† pada serikat pekerja terkadang bisa menjadi ambigu. Penugasan sihir hanya akan berlaku dalam kasus-kasus tertentu. Casing khusus itu hanya membuat Anda terkadang harus eksplisit.

Saya tidak melihat gunanya terkadang melewatkan satu langkah, terutama ketika perubahan kode dapat dengan mudah membatalkan kasus khusus itu dan kemudian Anda harus kembali dan memperbarui semua kode. Untuk menggunakan contoh Foo/Bar Anda jika C int ditambahkan ke Foo maka Bar(1) harus diubah tetapi tidak Bar("hello world") . Ini memperumit segalanya untuk menyimpan beberapa penekanan tombol dalam situasi yang mungkin tidak umum dan membuat konsep lebih sulit untuk dipahami karena terkadang terlihat seperti ini dan terkadang terlihat seperti itu—lihat saja diagram alur praktis ini untuk melihat mana yang sesuai untuk Anda!

Saya berharap saya memiliki nama yang lebih baik untuk itu. Sudah ada tag struct. Saya akan menyebutnya label tetapi Go juga memilikinya. Menyebutnya bidang tampaknya lebih tepat dan paling membingungkan. Kalau ada yang mau bikeshed yang satu ini bisa banget pakai yang fresh coat.

Dalam arti tertentu, serikat yang ditandai lebih mirip dengan struct daripada antarmuka. Mereka adalah jenis struct khusus yang hanya dapat memiliki satu set bidang pada satu waktu. Dilihat dalam terang itu, contoh Foo/Bar Anda akan seperti mengatakan ini:

type Foo struct {
  A string
  B int
}

func Bar(f Foo) {...}

func main() {
  Bar("hello world") //same as Bar(Foo{A: "hello world", B: 0})
  Bar(1) //same as Bar(Foo{A: "", B: 1})
}

Meskipun tidak ambigu dalam kasus ini, saya pikir itu bukan ide yang bagus.

Juga dalam proposal Bar(Foo{1}) diperbolehkan jika tidak ambigu jika Anda benar-benar ingin menyimpan penekanan tombol. Anda juga dapat memiliki pointer ke union sehingga sintaks literal komposit masih diperlukan untuk &Foo{"hello world"} .

Yang mengatakan, serikat pekerja memiliki kesamaan dengan antarmuka karena mereka memiliki tag dinamis yang "bidang" saat ini ditetapkan.

switch v := u.[type] {... dengan baik mencerminkan switch v := i.(type) {... untuk antarmuka sambil tetap mengizinkan sakelar tipe dan pernyataan langsung pada nilai gabungan. Mungkin seharusnya u.[union] untuk membuatnya lebih mudah dikenali, tetapi bagaimanapun juga sintaksnya tidak terlalu berat dan jelas apa yang dimaksud.

Anda dapat membuat argumen yang sama bahwa .(type) tidak diperlukan, tetapi ketika Anda melihat bahwa Anda selalu tahu persis apa yang terjadi dan itu sepenuhnya membenarkannya, menurut pendapat saya.

Itulah alasan saya di balik pilihan ini.

@jimmyfrasche
Sintaks sakelar tampaknya agak kontra-intuitif bagi saya, bahkan setelah penjelasan Anda. Dengan antarmuka, switch v := i.(type) {... beralih melalui kemungkinan jenis, seperti yang tercantum oleh kasus sakelar, dan ditunjukkan oleh .(type) .
Namun, dengan serikat pekerja, sakelar tidak beralih melalui jenis yang mungkin, tetapi nilai. Setiap kasus mewakili kemungkinan nilai yang berbeda, di mana nilai mungkin sebenarnya memiliki tipe yang sama. Ini lebih mirip dengan string dan sakelar int, di mana kasing juga mencantumkan nilai, dan sintaksnya adalah switch v := u {... . Dari itu, bagi saya tampaknya lebih alami bahwa beralih melalui nilai-nilai serikat akan menjadi switch v := u { ... , karena kasingnya serupa, tetapi lebih membatasi, daripada kasing untuk int dan string.

@urandom itu

switch u {... akan berhasil tetapi masalah dengan switch v := u {... adalah terlalu mirip switch v := f(); v {... (yang akan membuat pelaporan kesalahan lebih sulit—tidak jelas mana yang dimaksudkan).

Jika kata kunci union diganti namanya menjadi pick seperti yang disarankan oleh @as maka tag switch dapat ditulis sebagai switch u.[pick] {... atau switch v := u.[pick] {... yang menjaga simetri dengan sakelar tipe tetapi kehilangan kebingungan dan terlihat cukup bagus.

Bahkan jika implementasinya mengaktifkan int, masih ada perusakan implisit dari pick menjadi tag dinamis dan nilai tersimpan, yang menurut saya harus eksplisit, terlepas dari aturan tata bahasa

Anda tahu, hanya memanggil bidang tag dan menjadikannya sebagai penegasan bidang dan sakelar bidang sangat masuk akal.

edit: itu akan membuat penggunaan refleksi dengan pick menjadi canggung

[Maaf atas tanggapan yang tertunda - saya sedang pergi berlibur]

@ianlancetaylor menulis:

Sesuatu yang tidak saya lihat dalam proposal Anda adalah mengapa kita harus melakukan ini. Ada kerugian yang jelas untuk menambahkan tipe baru: ini adalah konsep baru yang harus dipelajari oleh setiap orang yang bersandar pada Go. Apa keuntungan kompensasinya? Secara khusus, apa yang diberikan tipe baru kepada kita yang tidak kita dapatkan dari tipe antarmuka?

Ada dua keuntungan utama yang saya lihat. Yang pertama adalah keunggulan bahasa; yang kedua adalah keunggulan kinerja.

  • saat memproses pesan, terutama saat dibaca dari proses bersamaan, sangat berguna untuk mengetahui set lengkap pesan yang dapat diterima, karena setiap pesan dapat datang dengan persyaratan protokol terkait. Untuk protokol tertentu, jumlah kemungkinan jenis pesan mungkin sangat kecil, tetapi ketika kami menggunakan antarmuka terbuka untuk mewakili pesan, invarian itu tidak jelas. Seringkali orang akan menggunakan saluran yang berbeda untuk setiap jenis pesan untuk menghindari hal ini, tetapi hal itu datang dengan biayanya sendiri.

  • ada kalanya ada sejumlah kecil kemungkinan jenis pesan yang diketahui, tidak ada satupun yang mengandung pointer. Jika kita menggunakan antarmuka terbuka untuk merepresentasikannya, kita perlu mengeluarkan alokasi untuk membuat nilai antarmuka. Menggunakan jenis yang membatasi kemungkinan jenis pesan berarti dapat dihindari dan karenanya mengurangi tekanan GC dan meningkatkan lokalitas cache.

Rasa sakit khusus bagi saya yang bisa dipecahkan oleh tipe sum adalah godoc. Ambil ast.Spec misalnya: https://golang.org/pkg/go/ast/#Spec

Banyak paket secara manual mencantumkan kemungkinan tipe dasar dari tipe antarmuka bernama, sehingga pengguna dapat dengan cepat mendapatkan ide tanpa harus melihat kode atau bergantung pada sufiks atau awalan nama.

Jika bahasa sudah mengetahui semua nilai yang mungkin, ini dapat diotomatisasi dalam godoc seperti tipe enum dengan iotas. Mereka juga dapat benar-benar menautkan ke jenisnya, bukan hanya teks biasa.

Sunting: contoh lain: https://github.com/mvdan/sh/commit/ebbfda50dfe167bee741460a4491ffec1006bdef

@mvdan itu

Maaf, apakah Anda hanya merujuk pada tautan ke nama lain di dalam halaman godoc, tetapi masih mencantumkannya secara manual?

Maaf, seharusnya lebih jelas.

Maksud saya permintaan fitur untuk secara otomatis menangani jenis yang mengimplementasikan antarmuka yang ditentukan dalam paket saat ini di godoc.

(Saya yakin ada permintaan fitur di suatu tempat untuk menautkan nama yang terdaftar secara manual, tetapi saya tidak punya waktu untuk memburunya saat ini).

Saya tidak ingin mengambil alih utas ini (sudah sangat panjang), jadi saya telah membuat masalah terpisah - lihat di atas.

@Merovius Saya membalas https://github.com/golang/go/issues/19814#issuecomment -298833986 dalam masalah ini karena hal AST lebih berlaku untuk tipe jumlah daripada enum. Maaf karena menarik Anda ke dalam masalah yang berbeda.

Pertama, saya ingin menegaskan kembali bahwa saya tidak yakin apakah tipe sum termasuk dalam Go. Saya belum meyakinkan diri sendiri bahwa mereka pasti tidak termasuk. Saya bekerja di bawah asumsi yang mereka lakukan untuk mengeksplorasi ide dan melihat apakah mereka cocok. Saya bersedia diyakinkan dengan cara apa pun.

Kedua, Anda menyebutkan perbaikan kode bertahap dalam komentar Anda. Menambahkan istilah baru ke tipe jumlah menurut definisi merupakan perubahan yang melanggar, setara dengan menambahkan metode baru ke antarmuka atau menghapus bidang dari struct. Tapi ini adalah perilaku yang benar dan diinginkan.

Mari kita pertimbangkan contoh AST, yang diimplementasikan dengan antarmuka Node, yang menambahkan jenis node baru. Katakanlah AST didefinisikan dalam proyek eksternal dan Anda mengimpornya dalam sebuah paket di proyek Anda, yang menjalankan AST.

Ada beberapa kasus:

  1. Kode Anda diharapkan berjalan di setiap simpul:
    1.1. Anda tidak memiliki pernyataan default, kode Anda salah secara diam-diam
    1.2. Anda memiliki pernyataan default dengan panik, kode Anda gagal saat runtime alih-alih waktu kompilasi (tes tidak membantu karena mereka hanya tahu tentang node yang ada saat Anda menulis tes)
  2. Kode Anda hanya memeriksa subset dari tipe node:
    2.1. Node jenis baru ini tidak akan ada di subset
    2.1.1. Selama simpul baru ini tidak pernah berisi simpul apa pun yang Anda minati, semuanya berhasil
    2.1.2. Jika tidak, Anda berada dalam situasi yang sama seolah-olah kode Anda diharapkan berjalan di setiap simpul
    2.2. Node jenis baru ini akan berada di subset yang Anda minati, seandainya Anda mengetahuinya.

Dengan AST berbasis antarmuka, hanya kasus 2.1.1 yang berfungsi dengan benar. Ini adalah kebetulan sebanyak apapun. Perbaikan kode bertahap tidak berfungsi. AST harus mengubah versinya dan kode Anda perlu mengubah versinya.

Linter yang lengkap akan membantu tetapi karena linter tidak dapat memeriksa semua jenis antarmuka, maka perlu diberi tahu dengan cara tertentu bahwa antarmuka tertentu perlu diperiksa. Itu berarti komentar sumber atau semacam file konfigurasi di repo Anda. Jika itu adalah komentar sumber, karena menurut definisi AST didefinisikan dalam proyek terpisah, Anda berada di bawah kekuasaan proyek itu untuk menandai antarmuka untuk pemeriksaan kelengkapan. Ini hanya berfungsi dalam skala jika ada satu linter lengkap yang disetujui dan selalu digunakan oleh seluruh komunitas.

Dengan AST berbasis jumlah, Anda masih perlu menggunakan pembuatan versi. Satu-satunya perbedaan dalam hal ini adalah bahwa linter kelengkapan dibangun ke dalam kompiler.

Tidak ada yang membantu dengan 2.2, tetapi apa yang bisa?

Ada kasus yang lebih sederhana, berdekatan dengan AST, di mana tipe jumlah akan berguna: token. Katakanlah Anda sedang menulis lexer untuk kalkulator sederhana. Ada token seperti * yang tidak memiliki nilai yang terkait dengannya dan token seperti Var yang memiliki string yang mewakili nama, dan token seperti Val yang menyimpan float64 .

Anda bisa mengimplementasikan ini dengan antarmuka tetapi itu akan melelahkan. Anda mungkin akan melakukan sesuatu seperti ini:

package token
type Type int
const (
  Times Type = iota
  // ...
  Var
  Val
)
type Value struct {
  Type
  Name string // only valid if Type == Var
  Number float64 // only valid if Type == Val
}

Linter yang lengkap pada enum berbasis iota dapat memastikan Jenis ilegal tidak pernah digunakan, tetapi itu tidak akan bekerja dengan baik terhadap seseorang yang menetapkan Nama saat Jenis == Kali atau menggunakan Nomor saat Jenis == Var. Ketika jumlah dan jenis token bertambah, itu hanya akan semakin buruk. Benar-benar yang terbaik yang dapat Anda lakukan di sini adalah menambahkan metode, Valid() error , yang memeriksa semua kendala dan banyak dokumentasi yang menjelaskan kapan Anda dapat melakukan apa.

Jenis sum dengan mudah mengkodekan semua kendala itu dan definisinya adalah semua dokumentasi yang diperlukan. Menambahkan jenis token baru akan menjadi perubahan besar tetapi semua yang saya katakan tentang AST masih berlaku di sini.

Saya pikir lebih banyak perkakas diperlukan. Saya hanya tidak yakin itu cukup.

@jimmyfrasche

Kedua, Anda menyebutkan perbaikan kode bertahap dalam komentar Anda. Menambahkan istilah baru ke tipe jumlah menurut definisi merupakan perubahan yang melanggar, setara dengan menambahkan metode baru ke antarmuka atau menghapus bidang dari struct.

Tidak, itu tidak setara. Anda dapat melakukan kedua perubahan tersebut dalam model perbaikan bertahap (untuk antarmuka: 1. Tambahkan metode baru ke semua implementasi, 2. Tambahkan metode ke antarmuka. Untuk bidang struct: 1. Hapus semua penggunaan bidang, 2. Hapus bidang). Menambahkan kasing dalam tipe jumlah tidak dapat berfungsi dalam model perbaikan bertahap; jika Anda menambahkannya lakukan lib terlebih dahulu, itu akan merusak semua pengguna, karena mereka tidak memeriksa secara mendalam lagi, tetapi Anda tidak dapat menambahkannya ke pengguna terlebih dahulu, karena kasing baru belum ada. Hal yang sama berlaku untuk penghapusan.

Ini bukan tentang apakah itu perubahan yang melanggar atau tidak, ini tentang apakah itu perubahan besar yang dapat diatur dengan gangguan minimal.

Tapi ini adalah perilaku yang benar dan diinginkan.

Tepat. Jenis penjumlahan, menurut definisinya dan setiap alasan orang menginginkannya, pada dasarnya tidak sesuai dengan gagasan perbaikan kode bertahap.

Dengan AST berbasis antarmuka, hanya kasus 2.1.1 yang berfungsi dengan benar.

Tidak, ini juga berfungsi dengan benar dalam kasus 1.2 (gagal saat runtime untuk tata bahasa yang tidak dikenal baik-baik saja. Saya mungkin tidak ingin panik, tetapi hanya mengembalikan kesalahan) dan juga dalam banyak kasus 2.1. Sisanya adalah masalah mendasar dengan peningkatan perangkat lunak; jika Anda menambahkan fitur baru ke perpustakaan, pengguna lib Anda perlu mengubah kode untuk menggunakannya. Namun, itu tidak berarti perangkat lunak Anda salah sampai

AST harus mengubah versinya dan kode Anda perlu mengubah versinya.

Saya tidak melihat bagaimana ini mengikuti dari apa yang Anda katakan, sama sekali. Bagi saya, mengatakan "tata bahasa baru ini belum berfungsi dengan semua alat, tetapi tersedia untuk kompiler" tidak masalah. Sama seperti "jika Anda menjalankan alat ini pada tata bahasa baru ini, itu akan gagal saat runtime" tidak masalah. Paling buruk, ini hanya menambahkan satu langkah lagi ke proses perbaikan bertahap: a) Tambahkan node baru ke paket AST dan parser. b) Perbaiki alat menggunakan paket AST untuk memanfaatkan node baru. c) Perbarui kode untuk menggunakan node baru. Ya, simpul baru hanya akan dapat digunakan, setelah a) dan b) selesai; tetapi dalam setiap langkah proses ini, tanpa ada kerusakan, semuanya akan tetap dikompilasi dan bekerja dengan benar.

Saya tidak mengatakan Anda akan secara otomatis baik-baik saja di dunia perbaikan kode bertahap dan tidak ada pemeriksaan kompiler yang lengkap. Itu masih memerlukan perencanaan dan eksekusi yang cermat, Anda mungkin masih akan mematahkan dependensi terbalik yang tidak terawat dan mungkin masih ada perubahan yang mungkin tidak dapat Anda lakukan sama sekali (meskipun saya tidak dapat memikirkannya). Tetapi setidaknya a) ada jalur peningkatan bertahap dan b) keputusan apakah ini akan merusak alat Anda saat runtime, atau tidak, terserah pembuat alat. Mereka dapat memutuskan apa yang harus dilakukan dalam kasus yang tidak diketahui.

Linter yang lengkap akan membantu tetapi karena linter tidak dapat memeriksa semua jenis antarmuka, maka perlu diberi tahu dengan cara tertentu bahwa antarmuka tertentu perlu diperiksa.

Mengapa? Saya berpendapat bahwa tidak masalah bagi switchlint™ untuk mengeluh tentang sakelar tipe apa pun tanpa kasing default; lagi pula, Anda mengharapkan kode untuk bekerja dengan definisi antarmuka apa pun, jadi tidak memiliki kode untuk bekerja dengan implementasi yang tidak diketahui kemungkinan besar merupakan masalah. Ya, ada pengecualian untuk aturan ini, tetapi pengecualian sudah dapat diabaikan secara manual.

Saya mungkin akan lebih siap dengan menegakkan "setiap sakelar tipe harus memerlukan case default, bahkan jika itu kosong" di kompiler, daripada dengan tipe jumlah yang sebenarnya. Itu akan memungkinkan dan memaksa orang untuk membuat keputusan tentang apa yang harus dilakukan kode mereka ketika dihadapkan dengan pilihan yang tidak diketahui.

Anda bisa mengimplementasikan ini dengan antarmuka tetapi itu akan melelahkan.

mengangkat bahu itu adalah upaya satu kali dalam kasus yang sangat jarang muncul. Sepertinya baik-baik saja bagi saya.

Dan FWIW, saat ini saya hanya menentang gagasan pemeriksaan lengkap tentang tipe jumlah. Saya belum memiliki pendapat yang kuat tentang kenyamanan tambahan untuk mengatakan "salah satu dari tipe yang ditentukan secara struktural ini".

@Merovius Saya harus berpikir lebih jauh tentang poin bagus Anda tentang perbaikan kode bertahap. Sementara itu:

pemeriksaan kelengkapan

Saat ini saya hanya berdebat melawan gagasan pemeriksaan lengkap tentang tipe jumlah.

Anda dapat secara eksplisit memilih keluar dari pemeriksaan kelengkapan dengan kasus default (baik, efektif: default membuatnya lengkap dengan menambahkan kasus yang mencakup "apa pun, apa pun itu"). Anda masih punya pilihan, tetapi Anda harus membuatnya secara eksplisit.

Saya berpendapat bahwa tidak masalah bagi switchlint™ untuk mengeluh tentang sakelar tipe apa pun tanpa kasing default; lagi pula, Anda mengharapkan kode untuk bekerja dengan definisi antarmuka apa pun, jadi tidak memiliki kode untuk bekerja dengan implementasi yang tidak diketahui kemungkinan besar merupakan masalah. Ya, ada pengecualian untuk aturan ini, tetapi pengecualian sudah dapat diabaikan secara manual.

Itu ide yang menarik. Meskipun itu akan mencapai jumlah jenis yang disimulasikan dengan antarmuka dan enum yang disimulasikan dengan const/iota, itu tidak memberi tahu Anda bahwa Anda melewatkan kasus yang diketahui, hanya saja Anda tidak menangani kasus yang tidak diketahui. Terlepas dari itu, tampaknya berisik. Mempertimbangkan:

switch {
case n < 0:
case n == 0:
case n > 0:
}

Itu lengkap jika n adalah integral (untuk float tidak ada n != n ) tetapi tanpa menyandikan banyak informasi tentang tipe, mungkin lebih mudah untuk menandainya sebagai default yang hilang. Untuk sesuatu seperti:

switch {
case p[0](a, b):
case p[1](a, b):
//...
case p[N](a, b):
}

bahkan jika p[i] membentuk relasi ekivalensi pada tipe a dan b itu tidak akan dapat membuktikannya, jadi ia harus menandai sakelar sebagai default case, yang berarti cara untuk membungkamnya dengan manifes, anotasi di sumber, skrip pembungkus untuk egrep -v keluar dari daftar putih, atau default yang tidak perlu pada sakelar yang secara keliru menyiratkan bahwa p[i] tidak lengkap.

Bagaimanapun itu akan menjadi linter sepele untuk diterapkan jika rute "selalu mengeluh tentang tidak ada default dalam semua keadaan" diambil. Akan menarik untuk melakukannya dan menjalankannya di go-corpus dan melihat seberapa berisik dan/atau bergunanya dalam praktiknya.

token

Implementasi token alternatif:

//Type defined as before
type SimpleToken { Type }
type StringToken { Type; Value string }
type NumberToken { Type; Value float64 }
type Interface interface {
  //some method common to all these types, maybe just token() Interface
}

Itu menghilangkan kemungkinan mendefinisikan status token ilegal di mana sesuatu memiliki nilai string dan angka tetapi tidak melarang pembuatan StringToken dengan tipe yang seharusnya SimpleToken atau sebaliknya sebaliknya.

Untuk melakukannya dengan antarmuka, Anda perlu mendefinisikan satu jenis per token ( type Plus struct{} , type Mul struct{} , dll.) dan sebagian besar definisi persis sama persis untuk nama jenis. Upaya satu kali atau tidak itu banyak pekerjaan (meskipun sangat cocok untuk pembuatan kode dalam kasus ini).

Saya kira Anda dapat memiliki "hierarki" antarmuka token untuk mempartisi jenis token berdasarkan nilai yang diizinkan: (Dengan asumsi dalam contoh ini ada lebih dari satu jenis token yang dapat berisi angka atau string, dll.)

type SimpleToken int //implements token.Interface
const (
  Plus SimpleToken = iota
  // ...
}
type NumericToken interface {
  Interface
  Value() float64
  nt() NumericToken
}
type IntToken struct { //implements NumericToken, and a FloatToken
type StringToken interface { // for Var and Func and Const, etc.
  Interface
  Value() string
  st() StringToken
}

Terlepas dari itu, itu berarti setiap token membutuhkan pointer deference untuk mengakses nilainya, tidak seperti tipe struct atau sum yang hanya membutuhkan pointer ketika string dilibatkan. Jadi dengan linter dan peningkatan yang sesuai untuk godoc, kemenangan besar untuk tipe jumlah dalam hal ini terkait dengan meminimalkan alokasi sambil melarang status ilegal dan jumlah pengetikan (dalam arti keyboard), yang sepertinya tidak penting.

Anda dapat secara eksplisit memilih keluar dari pemeriksaan kelengkapan dengan kasus default (baik, efektif: default membuatnya lengkap dengan menambahkan kasus yang mencakup "apa pun, apa pun itu"). Anda masih punya pilihan, tetapi Anda harus membuatnya secara eksplisit.

Jadi, sepertinya kita berdua akan memiliki pilihan untuk memilih masuk atau keluar dari pemeriksaan lengkap :)

itu tidak memberi tahu Anda bahwa Anda melewatkan kasus yang diketahui, hanya saja Anda tidak menangani kasus yang tidak diketahui.

Secara efektif, saya percaya, kompiler sudah melakukan analisis seluruh program untuk menentukan tipe konkret apa yang digunakan dalam antarmuka apa yang menurut saya ? Saya setidaknya mengharapkannya, setidaknya untuk pernyataan tipe non-antarmuka (yaitu pernyataan tipe yang tidak menyatakan ke tipe antarmuka, tetapi ke tipe konkret), menghasilkan tabel fungsi yang digunakan dalam antarmuka pada waktu kompilasi.
Tapi sejujurnya, ini argumen dari prinsip pertama, saya tidak tahu tentang implementasi yang sebenarnya.

Bagaimanapun, itu seharusnya cukup mudah, untuk a) membuat daftar tipe konkret apa pun yang ditentukan dalam keseluruhan program dan b) untuk sakelar tipe apa pun, memfilternya apakah mereka mengimplementasikan antarmuka itu. Jika Anda menggunakan sesuatu seperti ini , Anda akan mendapatkan daftar yang andal. Menurut saya.

Saya tidak 100% yakin bahwa alat dapat ditulis yang dapat diandalkan seperti yang secara eksplisit menyatakan opsi, tetapi saya yakin Anda dapat mencakup 90% kasus dan Anda pasti dapat menulis alat yang melakukan ini di luar compiler, diberikan penjelasan yang benar (yaitu membuat sum-jenis komentar pragma-seperti, bukan jenis yang sebenarnya). Bukan solusi yang bagus, memang.

Terlepas dari itu, tampaknya berisik. Mempertimbangkan:

Saya pikir ini tidak adil. Kasus-kasus yang Anda sebutkan sama sekali tidak ada hubungannya dengan tipe-jumlah. Jika saya menulis alat seperti itu di mana, saya akan membatasinya pada sakelar tipe dan sakelar dengan ekspresi, karena itu sepertinya cara jumlah tipe akan ditangani juga.

Implementasi token alternatif:

Mengapa bukan metode penanda? Anda tidak memerlukan bidang tipe, Anda mendapatkannya secara gratis dari representasi antarmuka. Jika Anda khawatir tentang mengulangi metode penanda berulang kali; definisikan struct yang tidak diekspor{}, berikan metode marker itu dan sematkan di setiap implementasi, tanpa biaya tambahan dan lebih sedikit pengetikan per opsi daripada metode Anda.

Terlepas dari itu, itu berarti setiap token memerlukan penunjukan untuk mengakses nilainya

Ya. Ini adalah biaya riil, tapi saya tidak berpikir itu pada dasarnya melebihi argumen lain.

Saya pikir ini tidak adil.

Itu benar.

Saya menulis versi cepat dan kotor dan menjalankannya di stdlib. Memeriksa pernyataan sakelar apa pun memiliki 1956 klik, membatasinya untuk melewati formulir switch { mengurangi jumlah itu menjadi 1677. Saya belum memeriksa lokasi mana pun untuk melihat apakah hasilnya bermakna.

https://github.com/jimmyfrasche/switchlint

Tentu saja ada banyak ruang untuk perbaikan. Ini tidak terlalu canggih. Permintaan tarik diterima.

(Selebihnya akan saya balas nanti)

edit: format markup salah

Saya pikir ini adalah ringkasan (cukup bias) dari semuanya sejauh ini (dan secara narsis mengasumsikan proposal kedua saya)

kelebihan

  • ringkas, mudah untuk menulis sejumlah kendala secara ringkas dengan cara yang mendokumentasikan diri sendiri
  • kontrol alokasi yang lebih baik
  • lebih mudah untuk dioptimalkan (semua kemungkinan diketahui oleh kompiler)
  • pemeriksaan lengkap (bila diinginkan, dapat memilih keluar)

Kontra

  • setiap perubahan pada anggota jenis jumlah adalah perubahan yang melanggar, melarang perbaikan kode bertahap kecuali semua paket eksternal memilih keluar dari pemeriksaan kelengkapan
  • satu hal lagi dalam bahasa untuk dipelajari, beberapa konseptual tumpang tindih dengan fitur yang ada
  • pengumpul sampah harus tahu anggota mana yang menjadi penunjuk
  • canggung untuk jumlah dari bentuk 1 + 1 + ⋯ + 1

Alternatif

  • iota "enum" untuk jumlah dari bentuk 1 + 1 + ⋯ + 1
  • antarmuka dengan metode tag yang tidak diekspor untuk jumlah yang lebih rumit (mungkin dihasilkan)
  • atau struct dengan sedikit enum dan aturan ekstra-linguistik tentang bidang mana yang ditetapkan bergantung pada nilai enum

Tanpa memedulikan

  • perkakas yang lebih baik, perkakas yang selalu lebih baik

Untuk perbaikan bertahap, dan itu yang besar, saya pikir satu-satunya pilihan adalah paket eksternal untuk memilih keluar dari pemeriksaan kelengkapan. Ini menyiratkan bahwa itu harus legal untuk memiliki kasus default "tidak perlu" hanya berkaitan dengan pemeriksaan di masa depan meskipun Anda sebaliknya cocok dengan yang lainnya. Saya percaya bahwa itu benar secara implisit sekarang, dan jika tidak cukup mudah untuk ditentukan.

Mungkin ada pengumuman dari pengelola paket bahwa "hei, kami akan menambahkan anggota baru ke jenis jumlah ini di versi berikutnya, pastikan Anda dapat menanganinya" dan kemudian alat switchlint dapat menemukan kasus apa pun yang perlu memilih keluar.

Tidak sesederhana kasus lain, tetapi masih cukup bisa dilakukan.

Saat menulis program yang menggunakan tipe penjumlahan yang ditentukan secara eksternal, Anda dapat mengomentari default untuk memastikan Anda tidak melewatkan kasus yang diketahui dan kemudian batalkan komentar sebelum melakukan. Atau mungkin ada alat untuk memberi tahu Anda bahwa defaultnya adalah "tidak perlu" yang memberi tahu Anda bahwa Anda mengetahui segalanya dan terbukti di masa depan terhadap yang tidak diketahui.

Katakanlah kita ingin ikut serta dalam pemeriksaan kelengkapan dengan linter saat menggunakan tipe antarmuka yang mensimulasikan tipe penjumlahan, terlepas dari paket yang mereka definisikan.

@Merovius trik betterSumType() BetterSumType sangat keren, tetapi itu berarti sakelar harus terjadi dalam paket yang menentukan (atau Anda mengekspos sesuatu seperti

func CallBeforeSwitches(b BetterSumType) (BetterSumType, bool) {
    if b == nil {
        return nil, false
    }
    b = b.betterSumType()
    if b == nil {
        return nil, false
    }
    return b, true
}

dan juga lint yang dipanggil setiap saat).

Apa kriteria yang diperlukan untuk memeriksa bahwa semua sakelar dalam suatu program sudah lengkap?

Itu tidak boleh berupa antarmuka kosong, karena apa pun itu permainan. Jadi dibutuhkan setidaknya satu metode.

Jika antarmuka tidak memiliki metode yang tidak diekspor, jenis apa pun dapat mengimplementasikannya sehingga kelengkapannya akan bergantung pada semua paket hingga callgraph setiap sakelar. Dimungkinkan untuk mengimpor paket, mengimplementasikan antarmukanya, dan kemudian mengirim nilai itu ke salah satu fungsi paket; jadi sakelar dalam fungsi itu tidak akan lengkap tanpa membuat siklus impor. Jadi perlu setidaknya satu metode yang tidak diekspor. (Ini termasuk kriteria sebelumnya).

Penyematan akan mengacaukan properti yang kita cari, jadi kita perlu memastikan bahwa tidak ada pengimpor paket yang pernah menyematkan antarmuka atau jenis apa pun yang mengimplementasikannya di titik mana pun. Linter yang sangat bagus mungkin dapat mengetahui bahwa terkadang penyematan tidak apa-apa jika kita tidak pernah memanggil fungsi tertentu yang menciptakan nilai yang disematkan atau tidak ada antarmuka yang disematkan yang pernah "melarikan diri" dari batas API paket.

Agar teliti, kita perlu memeriksa apakah nilai nol dari antarmuka tidak pernah dilewatkan atau memaksa sakelar lengkap memeriksa case nil juga. (Yang terakhir lebih mudah tetapi yang pertama lebih disukai karena memasukkan nil mengubah jumlah "tipe A atau tipe B atau tipe C" menjadi jumlah "nihil atau tipe A atau tipe B atau tipe C").

Katakanlah kita memiliki linter, dengan semua kemampuan itu, bahkan yang opsional, yang dapat memverifikasi semantik ini untuk pohon impor apa pun dan antarmuka apa pun di dalam pohon itu.

Sekarang katakanlah kita memiliki proyek dengan ketergantungan D. Kita ingin memastikan antarmuka yang didefinisikan dalam salah satu paket D sudah lengkap dalam proyek kita. Katakanlah itu benar.

Sekarang, kita perlu menambahkan ketergantungan baru ke proyek kita D′. Jika D′ mengimpor paket dalam D yang mendefinisikan tipe antarmuka yang dimaksud tetapi tidak menggunakan linter ini, ia dapat dengan mudah menghancurkan invarian yang perlu ditahan agar kita dapat menggunakan sakelar lengkap.

Dalam hal ini, katakanlah D hanya melewati linter secara kebetulan bukan karena pengelola menjalankannya. Upgrade ke D bisa dengan mudah menghancurkan invarian seperti D′.

Kalaupun linter bisa bilang "sekarang ini 100% lengkap 👍" itu bisa berubah tanpa kita berbuat apa-apa.

Pemeriksa kelengkapan untuk "iota enums" tampaknya lebih mudah.

Untuk semua type t u mana u adalah integral dan t digunakan sebagai const dengan nilai yang ditentukan secara individual atau iota sedemikian rupa sehingga nol nilai untuk u termasuk di antara konstanta ini.

Catatan:

  • Nilai duplikat dapat diperlakukan sebagai alias dan diabaikan dalam analisis ini. Kami akan menganggap semua konstanta bernama memiliki nilai yang berbeda.
  • 1 << iota dapat diperlakukan sebagai powerset, saya percaya setidaknya sebagian besar waktu, tetapi mungkin memerlukan kondisi tambahan, terutama di sekitar pelengkap bitwise. Untuk saat ini, mereka tidak akan dipertimbangkan

Untuk beberapa singkatan, mari kita panggil min(t) konstanta sedemikian rupa sehingga untuk konstanta lainnya, C , min(t) <= C , dan, dengan cara yang sama, sebut max(t) konstanta tersebut bahwa untuk konstanta lainnya, C , C <= max(t) .

Untuk memastikan t digunakan secara mendalam, kita perlu memastikan bahwa

  • nilai t selalu merupakan konstanta bernama (atau 0 dalam posisi idiomatik tertentu, seperti pemanggilan fungsi)
  • Tidak ada perbandingan pertidaksamaan dari nilai t , v , di luar min(t) <= v <= max(t)
  • nilai t tidak pernah digunakan dalam operasi aritmatika + , / , dll. Pengecualian yang mungkin adalah ketika hasilnya dijepit antara min(t) dan max(t) segera setelah itu, tetapi itu mungkin sulit dideteksi secara umum sehingga mungkin memerlukan anotasi dalam komentar dan mungkin harus dibatasi pada paket yang mendefinisikan t .
  • switch berisi semua konstanta t atau kasus default.

Ini masih memerlukan verifikasi semua paket di pohon impor dan dapat dibatalkan dengan mudah, meskipun cenderung tidak valid dalam kode idiomatik.

Pemahaman saya adalah bahwa ini, mirip dengan alias tipe, tidak akan merusak perubahan, jadi mengapa menahannya untuk Go 2?

Ketik alias tidak memperkenalkan kata kunci baru, yang merupakan perubahan yang pasti. Tampaknya juga ada moratorium bahkan untuk perubahan bahasa kecil dan ini akan menjadi perubahan besar . Bahkan hanya memasang kembali semua rutinitas marshal/unmarshal untuk menangani nilai jumlah yang direfleksikan akan menjadi cobaan berat.

Ketik alias sedang memperbaiki masalah yang tidak ada solusinya. Jenis sum memberikan manfaat dalam keamanan jenis, tetapi itu bukan penghenti acara yang tidak memilikinya.

Hanya satu poin (kecil) yang mendukung sesuatu seperti proposal asli @rogpeppe . Dalam paket http , ada tipe antarmuka Handler dan tipe fungsi yang mengimplementasikannya, HandlerFunc . Saat ini, untuk meneruskan fungsi ke http.Handle , Anda secara eksplisit harus mengonversinya menjadi HandlerFunc . Jika http.Handle sebagai gantinya menerima argumen tipe HandlerFunc | Handler , ia dapat menerima fungsi/penutupan apa pun yang dapat ditetapkan ke HandlerFunc secara langsung. Union secara efektif berfungsi sebagai petunjuk tipe yang memberi tahu kompiler bagaimana nilai dengan tipe yang tidak disebutkan namanya dapat dikonversi ke tipe antarmuka. Karena HandlerFunc mengimplementasikan Handler , tipe gabungan akan berperilaku persis seperti Handler sebaliknya.

@griesemer sebagai tanggapan atas komentar Anda di utas enum, https://github.com/golang/go/issues/19814#issuecomment -322752526, saya pikir proposal saya sebelumnya di utas ini https://github.com/golang/ go/issues/19412#issuecomment -289588569 menjawab pertanyaan tentang bagaimana tipe jumlah ("enum gaya cepat") harus bekerja di Go. Seperti aku ingin mereka, saya tidak tahu apakah mereka akan menjadi tambahan yang diperlukan untuk Go, tapi saya pikir jika mereka menambahkan mereka harus melihat / banyak seperti itu beroperasi.

Posting itu tidak lengkap dan ada klarifikasi di seluruh utas ini, sebelum dan sesudah, tetapi saya tidak keberatan mengulangi poin-poin itu atau meringkas karena utas ini cukup panjang.

Jika Anda memiliki tipe penjumlahan yang disimulasikan oleh antarmuka dengan tag tipe dan sama sekali tidak dapat menghindarinya dengan menyematkan, ini adalah pertahanan terbaik yang pernah saya buat: https://play.golang.org/p/FqdKfFojp-

@jimmyfrasche saya menulis ini beberapa waktu lalu.

Pendekatan lain yang mungkin adalah ini: https://play.golang.org/p/p2tFm984S8

@rogpeppe jika Anda akan menggunakan refleksi mengapa tidak menggunakan refleksi saja?

Saya telah menulis versi revisi proposal kedua saya berdasarkan komentar di sini dan di edisi lain.

Khususnya, saya telah menghapus pemeriksaan kelengkapan. Namun, pemeriksa kelengkapan eksternal adalah sepele untuk ditulis untuk proposal di bawah ini, meskipun saya tidak percaya seseorang dapat ditulis untuk tipe Go lain yang digunakan untuk mensimulasikan tipe penjumlahan.

Sunting: Saya telah menghapus kemampuan untuk mengetikkan pernyataan pada nilai dinamis dari nilai pilihan. Itu terlalu ajaib dan alasan untuk mengizinkannya dilayani dengan baik oleh pembuatan kode.

Sunting2: mengklarifikasi bagaimana nama bidang bekerja dengan pernyataan dan sakelar ketika pick ditentukan dalam paket lain.

Sunting3: penyematan terbatas dan nama bidang implisit yang diklarifikasi

Sunting4: klarifikasi default di sakelar

Pilih jenis

Pick adalah tipe komposit yang secara sintaksis mirip dengan struct:

pick {
  A, B S
  C, D T
  E U "a pick tag"
}

Di atas, A , B , C , D , dan E adalah nama bidang dari pick dan S , T , dan U adalah masing-masing jenis bidang tersebut. Nama bidang dapat diekspor atau tidak diekspor.

Sebuah pick mungkin tidak rekursif tanpa tipuan.

Hukum

type p pick {
    //...
    p *p
}

Liar

type p pick {
    //...
    p p
}

Tidak ada embedding untuk picks, tetapi pick mungkin disematkan dalam struct. Jika pick disematkan dalam struct, metode pada pick dipromosikan ke struct tetapi bidang pick tidak.

Jenis tanpa nama bidang adalah singkatan untuk mendefinisikan bidang dengan nama yang sama dengan jenisnya. (Ini adalah kesalahan jika jenisnya tidak disebutkan namanya, dengan pengecualian untuk *T mana namanya adalah T ).

Sebagai contoh,

type p pick {
    io.Reader
    io.Writer
    string
}

memiliki tiga bidang Reader , Writer , dan string , dengan tipe masing-masing. Perhatikan bahwa bidang string tidak diekspor meskipun dalam lingkup semesta.

Nilai tipe pick terdiri dari bidang dinamis dan nilai bidang itu.

Nilai nol dari jenis pick adalah bidang pertamanya dalam urutan sumber dan nilai nol bidang itu.

Diberikan dua nilai dari jenis pick yang sama, a dan b , nilai pick dapat ditetapkan sebagai nilai lainnya

a = b

Menetapkan nilai non-pilihan, bahkan salah satu dari jenis salah satu bidang dalam pilihan, adalah ilegal.

Jenis pick hanya memiliki satu bidang dinamis pada waktu tertentu.

Sintaks literal komposit mirip dengan struct, tetapi ada batasan tambahan. Yaitu, literal tanpa kunci selalu tidak valid dan hanya satu kunci yang dapat ditentukan.

Berikut ini adalah valid

pick{A string; B int}{A: "string"} //value is (B, "string")
pick{A, B int}{B: 1} //value is (B, 1)
pick{A, B string}{} //value is (A, "")

Berikut ini adalah kesalahan waktu kompilasi:

pick{A int; B string}{A: 1, B: "string"} //a pick can only have one value at a time
pick{A int; B uint}{1} //pick composite literals must be keyed

Diberikan nilai p dari tipe pick {A int; B string} tugas berikut

p.B = "hi"

menyetel bidang dinamis p menjadi B dan nilai B menjadi "hi".

Penugasan ke bidang dinamis saat ini memperbarui nilai bidang itu. Tugas yang menetapkan bidang dinamis baru harus nol lokasi memori yang tidak ditentukan. Penugasan ke bidang pick atau struct dari bidang pick memperbarui atau menyetel bidang dinamis seperlunya.

type P pick {
    A, B image.Point
}

var p P
fmt.Println(P) //{A: {0 0}}

p.A.X = 1 //A is the dynamic field, update
fmt.Println(P) //{A: {1 0}}

p.B.Y = 2 //B is not the dynamic value, create zero image.Point first
fmt.Println(P) //{B: {0 2}}

Nilai yang disimpan dalam pick hanya dapat diakses oleh field assert atau field switch.

x := p.[X] //panics if X is not the dynamic field of p
x, ok := p.[X] //returns the zero value of X and false if X is not the dynamic field of p

switch v := p.[var] {
case A:
case B, C: // v is only defined in this case if fields B and C have identical type names
case D:
default: // always legal even if all fields are exhaustively listed above
}

Nama bidang dalam pernyataan bidang dan sakelar bidang adalah properti dari tipe tersebut, bukan paket yang mendefinisikannya. Mereka tidak, dan tidak dapat, dikualifikasikan oleh nama paket yang mendefinisikan pick .

Ini berlaku:

_, ok := externalPackage.ReturnsPick().[Field]

Ini tidak valid:

_, ok := externalPackage.ReturnsPick().[externalPackage.Field]

Pernyataan bidang dan sakelar bidang selalu mengembalikan salinan nilai bidang dinamis.

Nama bidang yang tidak diekspor hanya dapat ditegaskan dalam paket definisinya.

Ketikkan pernyataan dan sakelar jenis juga berfungsi pada pilihan.

//removed, see note at top
//v, ok := p.(fmt.Stringer) //holds if the type of the dynamic field implements fmt.Stringer
//v, ok := p.(int) //holds if the type of the dynamic field is an int

Jenis pernyataan dan sakelar jenis selalu mengembalikan salinan nilai bidang dinamis.

Jika pick disimpan dalam sebuah antarmuka, ketik pernyataan untuk antarmuka hanya cocok dengan kumpulan metode dari pick itu sendiri. [masih benar tetapi berlebihan karena di atas telah dihapus]

Jika semua jenis pick mendukung operator kesetaraan maka:

  • nilai dari pick itu dapat digunakan sebagai kunci peta
  • dua nilai dari pick yang sama adalah == jika mereka memiliki bidang dinamis yang sama dan nilainya adalah ==
  • dua nilai dengan bidang dinamis yang berbeda adalah != meskipun nilainya == .

Tidak ada operator lain yang didukung pada nilai tipe pick.

Nilai dari tipe pick P dapat dikonversi ke tipe pick lain Q jika kumpulan nama field dan tipenya di P adalah subset dari nama field dan ketik Q .

Jika P dan Q didefinisikan dalam paket yang berbeda dan memiliki bidang yang tidak diekspor, bidang tersebut dianggap berbeda terlepas dari nama dan jenisnya.

Contoh:

type P pick {A int; B string}
type Q pick {B string; A int; C float64}

//legal
var p P
q := Q(p)

//illegal
var q Q
p := P(Q) //cannot handle field C

Penetapan antara dua jenis pick didefinisikan sebagai konvertibilitas, selama tidak lebih dari satu jenis didefinisikan.

Metode dapat dideklarasikan pada tipe pick yang ditentukan.

Saya membuat (dan menambahkan ke wiki) laporan pengalaman https://Gist.github.com/jimmyfrasche/ba2b709cdc390585ba8c43c989797325

Sunting: dan :hati: kepada @mewmew yang meninggalkan laporan yang jauh lebih baik dan lebih rinci sebagai balasan atas inti itu

Bagaimana jika kita memiliki cara untuk mengatakan, untuk tipe tertentu T , daftar tipe yang dapat dikonversi menjadi tipe T atau ditetapkan ke variabel tipe T ? Sebagai contoh

type T interface{} restrict { string, error }

mendefinisikan tipe antarmuka kosong bernama T sedemikian rupa sehingga satu-satunya tipe yang dapat ditetapkan padanya adalah string atau error . Setiap upaya untuk menetapkan nilai dari jenis lain menghasilkan kesalahan waktu kompilasi. Sekarang saya bisa mengatakan

func FindOrFail(m map[int]string, key int) T {
    if v, ok := m[key]; ok {
        return v
    }
    return errors.New("no such key")
}

func Lookup() {
    v := FindOrFail(m, key)
    if err, ok := v.(error); ok {
        log.Fatal(err)
    }
    s := v.(string) // This type assertion must succeed.
}

Elemen kunci apa dari tipe sum (atau tipe pick) yang tidak akan dipenuhi oleh pendekatan semacam ini?

s := v.(string) // This type assertion must succeed.

Ini tidak sepenuhnya benar, karena v juga bisa menjadi nil . Dibutuhkan perubahan yang cukup besar pada bahasa untuk menghilangkan kemungkinan ini, karena itu berarti memperkenalkan tipe yang tidak memiliki nilai nol dan semua yang diperlukan. Nilai nol menyederhanakan bagian bahasa, tetapi juga membuat perancangan fitur semacam ini lebih sulit.

Menariknya, pendekatan ini cukup mirip dengan proposal asli @rogpeppe . Apa yang tidak dimilikinya adalah pemaksaan terhadap tipe yang terdaftar, yang dapat berguna dalam situasi seperti yang saya tunjukkan sebelumnya ( http.Handler ). Hal lain adalah bahwa ia mengharuskan setiap varian untuk menjadi tipe yang berbeda, karena varian didiskriminasikan berdasarkan tipe daripada tag yang berbeda. Saya pikir ini benar-benar ekspresif, tetapi beberapa orang lebih suka memiliki tag dan tipe varian yang berbeda.

@ianlancetaylor

pro

  • mungkin untuk membatasi pada satu set tipe tertutup — dan itu jelas merupakan hal utama
  • mungkin untuk menulis pemeriksa kelengkapan yang tepat
  • Anda mendapatkan properti "Anda dapat menetapkan nilai yang memenuhi kontrak untuk ini". (Saya tidak peduli tentang ini, tapi saya membayangkan orang lain peduli).

kontra

  • mereka hanya antarmuka dengan manfaat dan bukan jenis yang benar-benar berbeda (manfaat yang bagus!)
  • anda masih memiliki nil jadi itu bukan tipe penjumlahan dalam pengertian teoretis tipe. Apa pun A + B + C Anda tentukan sebenarnya adalah 1 + A + B + C yang Anda tidak punya pilihan. Seperti yang ditunjukkan @stevenblenkinsop saat saya mengerjakan ini.
  • lebih penting lagi, karena pointer implisit itu Anda selalu memiliki tipuan. Dengan proposal pemilihan, Anda dapat memilih untuk memiliki p atau *p memberi Anda kontrol lebih besar atas pertukaran memori. Anda tidak dapat menerapkannya sebagai serikat yang didiskriminasi (dalam arti C) sebagai pengoptimalan.
  • tidak ada pilihan nilai nol, yang merupakan properti yang sangat bagus terutama karena sangat penting di Go untuk memiliki nilai nol yang berguna mungkin
  • mungkin Anda tidak dapat mendefinisikan metode pada T (tetapi mungkin Anda akan memiliki metode antarmuka yang dimodifikasi oleh batasan tetapi tipe dalam batasan harus memenuhinya? Kalau tidak, saya tidak mengerti maksudnya tidak hanya memiliki type T restrict {string, error} )
  • jika Anda kehilangan label untuk field/summands/what-have-you, maka itu akan membingungkan ketika berinteraksi dengan tipe antarmuka. Anda kehilangan properti "persis ini atau persis itu" yang kuat dari tipe jumlah. Anda dapat memasukkan io.Reader dan menarik io.Writer keluar. Itu masuk akal untuk antarmuka (tidak terbatas) tetapi bukan tipe jumlah.
  • Jika Anda ingin memiliki dua tipe identik yang berarti hal yang berbeda, Anda perlu menggunakan tipe pembungkus untuk membedakan; tag seperti itu harus berada di namespace luar daripada terbatas pada tipe seperti bidang struct
  • ini mungkin terlalu banyak membaca kata-kata spesifik Anda, tetapi sepertinya itu mengubah aturan penugasan berdasarkan jenis penerima tugas (saya membacanya dengan mengatakan Anda tidak dapat menetapkan sesuatu yang dapat ditugaskan ke error ke T harus benar-benar kesalahan).

Yang mengatakan, itu tidak mencentang kotak utama (dua pro pertama yang saya daftarkan) dan saya akan mengambilnya dalam sekejap jika hanya itu yang bisa saya dapatkan. Saya berharap untuk lebih baik, meskipun.

Saya berasumsi aturan penegasan tipe diterapkan. Jadi tipenya harus identik dengan tipe konkret atau dapat dialihkan ke tipe antarmuka. Pada dasarnya, ini bekerja persis seperti antarmuka tetapi nilai apa pun (selain nil ) harus dapat ditegaskan setidaknya untuk salah satu jenis yang terdaftar.

@jimmyfrasche
Dalam proposal Anda yang diperbarui, apakah penetapan berikut mungkin dilakukan, jika semua elemen dari jenis tersebut memiliki jenis yang berbeda:

type p pick {
    A int
    B string
}

func Foo(P p) {
}

var P p = 42
var Q p = "foo"

Foo(42)
Foo("foo")

Kegunaan tipe sum ketika penugasan seperti itu dimungkinkan jauh lebih besar.

Dengan proposal pemilihan, Anda dapat memilih untuk memiliki p atau *p memberi Anda kontrol lebih besar atas pertukaran memori.

Alasan mengapa antarmuka mengalokasikan untuk menyimpan nilai skalar adalah agar Anda tidak perlu membaca kata tipe untuk memutuskan apakah kata lain adalah pointer; lihat #8405 untuk diskusi. Pertimbangan implementasi yang sama kemungkinan akan berlaku untuk jenis pick, yang mungkin berarti dalam praktiknya p akhirnya mengalokasikan dan tetap non-lokal.

@urandom tidak, mengingat definisi Anda, itu harus ditulis

var p P = P{A: 42} // p := P{A: 42}
var q P = P{B: "foo")
Foo(P{A: 42}) // or Foo({A: 42}) if types can be elided here
Foo(P{B: "foo"})

Yang terbaik adalah menganggapnya sebagai struct yang hanya dapat memiliki satu set bidang pada satu waktu.

Jika Anda tidak memilikinya dan kemudian Anda menambahkan C uint ke p apa yang terjadi pada p = 42 ?

Anda dapat membuat banyak aturan berdasarkan urutan dan penugasan tetapi itu selalu berarti bahwa perubahan pada definisi tipe dapat memiliki efek yang halus dan dramatis pada semua kode yang menggunakan tipe tersebut.

Dalam kasus terbaik, perubahan merusak semua kode dengan mengandalkan kurangnya ambiguitas dan mengatakan Anda perlu mengubahnya menjadi p = int(42) atau p = uint(42) sebelum dikompilasi lagi. Perubahan satu baris seharusnya tidak memerlukan perbaikan seratus baris. Terutama jika baris-baris itu ada dalam paket orang tergantung pada kode Anda.

Anda juga harus 100% eksplisit atau memiliki tipe yang sangat rapuh yang tidak dapat disentuh oleh siapa pun karena dapat merusak segalanya.

Ini berlaku untuk semua jenis proposal jumlah tetapi jika ada label eksplisit, Anda masih memiliki kemampuan untuk ditetapkan karena label tersebut secara eksplisit tentang jenis yang ditetapkan.

@josharian jadi jika saya membacanya dengan benar, alasan iface sekarang selalu (*type, *value) alih-alih menyimpan nilai berukuran Word di bidang kedua seperti yang dilakukan Go sebelumnya adalah agar GC bersamaan tidak perlu memeriksa keduanya bidang untuk melihat apakah yang kedua adalah penunjuk—itu hanya bisa berasumsi bahwa itu selalu. Apakah saya benar?

Dengan kata lain, jika tipe pick diimplementasikan (menggunakan notasi C) seperti

struct {
    int which;
    union {
         A a;
         B b;
         C c;
    } summands;
}

GC perlu mengambil kunci (atau sesuatu yang mewah tetapi setara) untuk memeriksa which untuk menentukan apakah summands perlu dipindai?

alasan iface sekarang selalu (*type, *value) alih-alih menyimpan nilai berukuran Word di bidang kedua seperti yang dilakukan Go sebelumnya adalah agar GC bersamaan tidak perlu memeriksa kedua bidang untuk melihat apakah yang kedua adalah penunjuk —itu hanya bisa berasumsi bahwa selalu begitu.

Betul sekali.

Tentu saja, sifat terbatas dari jenis pick akan memungkinkan beberapa implementasi alternatif. Jenis pick dapat ditata sedemikian rupa sehingga selalu ada pola pointer/non-pointer yang konsisten; misalnya semua jenis skalar dapat tumpang tindih, dan bidang string dapat tumpang tindih dengan awal bidang irisan (karena keduanya memulai "penunjuk, bukan penunjuk"). Jadi

pick {
  a uintptr
  b string
  c []byte
}

kira-kira dapat dijabarkan:

[ word 1 (ptr) ] [ word 2 (non-ptr) ] [ word 3 (non-ptr) ]
[    <nil>         ] [                 a           ] [                              ]
[       b.ptr      ] [            b.len          ] [                              ]
[       c.ptr      ] [             c.len         ] [        c.cap             ]

tetapi jenis pick lainnya mungkin tidak memungkinkan pengemasan yang optimal. (Maaf tentang ASCII yang rusak, sepertinya saya tidak bisa membuat GitHub merendernya dengan benar. Saya harap Anda mengerti maksudnya.)

Kemampuan untuk melakukan tata letak statis ini bahkan mungkin menjadi argumen kinerja yang mendukung menyertakan tipe pick; tujuan saya di sini hanyalah untuk menandai detail implementasi yang relevan untuk Anda.

@josharian dan terima kasih telah melakukannya. Saya tidak memikirkan itu (sejujurnya saya baru saja mencari di Google jika ada penelitian tentang bagaimana serikat pekerja yang didiskriminasi GC, melihat bahwa ya Anda dapat melakukan itu dan menyebutnya suatu hari — untuk beberapa alasan otak saya tidak mengaitkan "konkurensi" dengan "Pergi" hari itu: facepalm!).

Akan ada lebih sedikit pilihan jika salah satu tipe struct yang ditentukan sudah memiliki tata letak.

Salah satu opsi adalah tidak "memadat" summands jika mengandung pointer yang berarti bahwa ukurannya akan sama dengan struct yang setara (+1 untuk int diskriminator). Mungkin mengambil pendekatan hibrida, jika memungkinkan, sehingga semua jenis yang dapat berbagi tata letak melakukannya.

Sayang sekali kehilangan properti ukuran yang bagus tetapi itu sebenarnya hanya pengoptimalan.

Bahkan jika itu selalu 1 + ukuran struct yang setara bahkan ketika mereka tidak mengandung pointer, itu masih akan memiliki semua properti bagus lainnya dari tipe itu sendiri, termasuk kontrol atas alokasi. Pengoptimalan tambahan dapat ditambahkan dari waktu ke waktu dan setidaknya dimungkinkan seperti yang Anda tunjukkan.

type p pick {
    A int
    B string
}

Apakah A dan B harus ada? Sebuah pick mengambil dari satu set jenis, jadi mengapa tidak membuang nama pengenal mereka sepenuhnya:

type p pick {
    int
    string
}
q := p{string: "hello"}

Saya percaya formulir ini sudah valid untuk struct. Mungkin ada kendala yang diperlukan untuk memilih.

@ seolah-olah nama bidang dihilangkan, itu sama dengan jenisnya sehingga contoh Anda berfungsi, tetapi karena nama bidang tersebut tidak diekspor, mereka hanya dapat diatur/diakses dari dalam paket yang ditentukan.

Nama bidang memang harus ada di sana, bahkan jika dibuat secara implisit berdasarkan nama jenis, atau ada interaksi yang buruk dengan kemampuan penetapan dan jenis antarmuka. Nama bidang inilah yang membuatnya bekerja dengan sisa Go.

@ sebagai permintaan maaf, saya baru menyadari maksud Anda berbeda dari apa yang saya baca.

Formulasi Anda berfungsi tetapi kemudian Anda memiliki hal-hal yang terlihat seperti bidang struct tetapi berperilaku berbeda karena hal yang biasa diekspor/tidak diekspor.

Apakah string dapat diakses dari luar paket yang mendefinisikan p karena ada di alam semesta?

Bagaimana dengan

type t struct {}
type P pick {
  t
  //other stuff
}

?

Dengan memisahkan nama bidang dari nama jenis Anda dapat melakukan hal-hal seperti

pick {
  unexported Exported
  Exported unexported
}

atau bahkan

pick { Recoverable, Fatal error }

Jika bidang pick berperilaku seperti bidang struct, Anda dapat menggunakan banyak dari apa yang sudah Anda ketahui tentang bidang struct untuk memikirkan bidang pick. Satu-satunya perbedaan nyata adalah bahwa hanya satu bidang yang dapat dipilih yang dapat diatur pada satu waktu.

@jimmyfrasche
Go sudah mendukung penyematan tipe anonim di dalam struct, jadi batasan ruang lingkup adalah salah satu yang sudah ada dalam bahasa, dan saya percaya bahwa masalah sedang diselesaikan dengan alias tipe. Tapi akui saya belum memikirkan setiap kasus penggunaan yang mungkin. Tampaknya bergantung pada apakah idiom ini umum di Go:

package p
type T struct{
    Exported t
}
type t struct{}

_t_ kecil ada dalam paket yang disematkan dalam T besar, dan eksposurnya hanya melalui jenis yang diekspor tersebut.

@sebagai

Saya tidak yakin saya sepenuhnya mengikuti, namun:

//with the option to have field names
pick { //T is in the namespace of the pick and the type isn't exposed to other packages
  T t
  //...
}

//without
type T = t //T has to be defined in the outer scope and now t is exposed to other packages
pick {
  T
  //...
}

Juga, jika Anda hanya memiliki nama jenis untuk label, untuk memasukkan katakanlah []string Anda harus melakukan type Strings = []string .

Itulah cara saya ingin melihat tipe pick diimplementasikan. Di dalam
khususnya, begitulah cara Rust dan C++ (standar emas untuk kinerja)
dia.

Jika saya hanya ingin memeriksa kelengkapan, saya bisa menggunakan checker. aku ingin
kinerja menang. Itu berarti jenis pick juga tidak boleh nihil.

Mengambil alamat anggota elemen pick seharusnya tidak diperbolehkan (itu
tidak aman untuk memori, bahkan dalam kasus single-threaded, seperti yang terkenal di
komunitas Karat.). Jika itu memerlukan batasan lain pada jenis pick,
maka jadilah itu. Tetapi bagi saya yang memiliki tipe pick selalu mengalokasikan di heap
akan buruk.

Pada 18 Agustus 2017 12:01, "jimmyfrasche" [email protected] menulis:

@josharian https://github.com/josharian jadi jika saya membacanya dengan benar
alasan iface sekarang selalu (*type, *value) alih-alih menyimpan
nilai ukuran kata di bidang kedua seperti yang dilakukan Go sebelumnya adalah agar
GC bersamaan tidak perlu memeriksa kedua bidang untuk melihat apakah yang kedua
adalah sebuah pointer—itu hanya dapat mengasumsikan bahwa itu selalu. Apakah saya benar?

Dengan kata lain, jika tipe pick diimplementasikan (menggunakan notasi C) seperti

struktur {
int yang;
Persatuan {
A A;
Bb;
Cc;
} perintah;
}

GC perlu mengambil kunci (atau sesuatu yang mewah tetapi setara) untuk
memeriksa yang mana untuk menentukan apakah perintah perlu dipindai?


Anda menerima ini karena Anda yang menulis utas.
Balas email ini secara langsung, lihat di GitHub
https://github.com/golang/go/issues/19412#issuecomment-323393003 , atau bisukan
benang
https://github.com/notifications/unsubscribe-auth/AGGWB3Ayi31dYwotewcfgmCQL-XVrfxIks5sZbVrgaJpZM4MTmSr
.

@DemiMarie

Mengambil alamat anggota elemen pick seharusnya tidak diperbolehkan (ini tidak aman untuk memori, bahkan dalam kasus single-threaded, seperti yang terkenal di komunitas Rust.). Jika itu memerlukan batasan lain pada jenis pick, maka biarlah.

Itu poin yang bagus. Saya memilikinya di sana tetapi pasti hilang saat diedit. Saya memang menyertakan bahwa ketika Anda mengakses nilai dari pick, ia selalu mengembalikan salinan untuk alasan yang sama.

Sebagai contoh mengapa itu benar, untuk anak cucu, pertimbangkan

v := pick{ A int; B bool }{A: 5}
p := &v.[A] //(this would be illegal but pretending it's not for a second)
v.B = true

Jika v dioptimalkan sehingga bidang A dan B mengambil posisi yang sama di memori maka p tidak menunjuk ke int: itu menunjuk ke bool. Keamanan memori dilanggar.

@jimmyfrasche

Alasan kedua Anda tidak ingin konten dapat dialamatkan adalah semantik mutasi. Jika nilai disimpan secara tidak langsung dalam keadaan tertentu, maka

v := pick{ A int; ... }{A: 5}
v2 := v

v2.[A] = 6 // (this would be illegal under the proposal, but would be 
           // permitted if `v2.[A]` were addressable)

fmt.Println(v.[A]) // would print 6 if the contents of the pick are stored indirectly

Satu tempat di mana pick mirip dengan antarmuka adalah Anda ingin mempertahankan semantik nilai jika Anda menyimpan nilai di dalamnya. Jika Anda mungkin memerlukan tipuan sebagai detail implementasi, satu-satunya pilihan adalah membuat konten tidak dapat dialamatkan (atau lebih tepatnya, dapat dialamatkan, tetapi perbedaannya tidak ada di Go saat ini), sehingga Anda tidak dapat mengamati aliasing .

Sunting: Ups (lihat di bawah)

@jimmyfrasche

Nilai nol dari jenis pick adalah bidang pertamanya dalam urutan sumber dan nilai nol bidang itu.

Perhatikan bahwa ini tidak akan berfungsi jika bidang pertama perlu disimpan secara tidak langsung, kecuali jika Anda memberi huruf besar pada nilai nol sehingga v.[A] dan v.(error) melakukan hal yang benar.

@stevenblenkinsop Saya tidak yakin apa yang Anda maksud dengan "bidang pertama harus disimpan secara tidak langsung". Saya berasumsi maksud Anda jika bidang pertama adalah pointer atau tipe yang secara implisit berisi pointer. Jika demikian, ada contoh di bawah ini. Jika tidak, bisa tolong jelaskan?

Diberikan

var p pick { A error; B int }

nilai nol, p , memiliki bidang dinamis A dan nilai A adalah nihil.

Saya tidak mengacu pada nilai yang disimpan dalam pick menjadi/berisi pointer, saya mengacu pada nilai non-pointer yang disimpan secara tidak langsung karena kendala tata letak yang dikenakan oleh pengumpul sampah, seperti yang dijelaskan oleh @josharian .

Dalam contoh Anda, p.B —tidak menjadi pointer—tidak akan dapat berbagi penyimpanan yang tumpang tindih dengan p.A , yang terdiri dari dua pointer. Kemungkinan besar harus disimpan secara tidak langsung (yaitu direpresentasikan sebagai *int yang secara otomatis mengalami dereferensi ketika Anda mengaksesnya, bukan sebagai int ). Jika p.B adalah bidang pertama, nilai nol dari pick akan menjadi new(int) , yang bukan merupakan nilai nol yang dapat diterima karena memerlukan inisialisasi. Anda harus membuat case khusus agar nil *int diperlakukan sebagai new(int) .

@jimmyfrasche
Oh maaf. Kembali ke percakapan, saya menyadari Anda sedang mempertimbangkan untuk menggunakan penyimpanan yang berdekatan untuk menyimpan varian dengan tata letak yang tidak kompatibel, daripada menyalin mekanisme antarmuka penyimpanan tidak langsung jenis non-pointer. Tiga komentar terakhir saya tidak masuk akal dalam kasus itu.

Sunting: ups, kondisi balapan. Diposting kemudian melihat komentar Anda.

@stevenblenkinsop ah, oke saya mengerti maksud Anda. Tapi itu tidak masalah.

Berbagi penyimpanan yang tumpang tindih adalah pengoptimalan. Itu tidak akan pernah bisa melakukan itu: semantik tipe adalah bagian yang penting.

Jika kompiler dapat mengoptimalkan penyimpanan dan memilih untuk melakukannya, itu adalah bonus yang bagus.

Dalam contoh Anda, kompiler dapat menyimpannya persis seperti struct yang setara (menambahkan tag untuk mengetahui bidang mana yang aktif). Ini akan menjadi

struct {
  which_field int // 0 = A, 1 = B
  A error
  B int
}

Nilai nol masih semua byte 0 dan tidak perlu secara diam-diam mengalokasikan sebagai kasus khusus.

Bagian penting adalah memastikan bahwa hanya satu bidang yang dimainkan pada waktu tertentu.

Motivasi untuk mengizinkan penegasan tipe/pengalihan pada pilihan adalah agar, misalnya, jika setiap jenis dalam pilihan memenuhi fmt.Stringer Anda dapat menulis metode pada pilihan seperti

func (p P) String() string {
  return p.(fmt.Stringer).String()
}

Tetapi karena jenis bidang pilihan dapat berupa antarmuka, ini menciptakan kehalusan.

Jika pick P pada contoh sebelumnya memiliki bidang yang tipenya sendiri fmt.Stringer String metode nil . Anda tidak dapat mengetikkan assert antarmuka nil untuk apa pun, bahkan dirinya sendiri. https://play.golang.org/p/HMYglwyVbl Meskipun ini selalu benar, itu tidak muncul secara teratur, tetapi bisa muncul lebih teratur dengan pilihan.

Namun, sifat tertutup dari tipe jumlah akan memungkinkan linter yang lengkap untuk menemukan di mana-mana ini akan muncul (berpotensi dengan beberapa positif palsu) dan melaporkan kasus yang perlu ditangani.

Ini juga akan mengejutkan, jika Anda dapat menerapkan metode pada pilihan, bahwa metode tersebut tidak digunakan untuk memenuhi pernyataan tipe.

type Num pick { A int; B float32 }

func (n Num) String() string {
      switch v := n.[var] {
      case A:
          return fmt.Sprint(v)
      case B:
          return fmt.Sprint(v)
      }
}
...
n := Num{A: 5}
s1, ok := p.(fmt.Stringer) // ok == false
var i interface{} = p
s2, ok := i.(fmt.Stringer) // ok == true

Anda dapat meminta metode promosi jenis pernyataan dari bidang saat ini jika memenuhi antarmuka, tetapi ini mengalami masalah sendiri, seperti apakah akan mempromosikan metode dari nilai dalam bidang antarmuka yang tidak ditentukan pada antarmuka itu sendiri (atau bahkan bagaimana menerapkan ini secara efisien). Juga, orang kemudian mungkin mengharapkan metode yang umum untuk semua bidang untuk dipromosikan ke pick itu sendiri, tetapi kemudian mereka harus dikirim melalui pemilihan varian pada setiap panggilan, selain berpotensi pengiriman virtual jika pick disimpan dalam antarmuka , dan/atau ke pengiriman virtual jika bidangnya adalah antarmuka.

Sunting: Omong-omong, mengemas pick secara optimal adalah contoh dari masalah superstring umum terpendek , yaitu NP-complete, meskipun ada perkiraan serakah yang umum digunakan.

Aturannya adalah jika itu adalah nilai pick, pernyataan tipe menegaskan pada bidang dinamis dari nilai pick, tetapi jika nilai pick disimpan dalam antarmuka, pernyataan tipe ada di kumpulan metode dari tipe pick. Ini mungkin mengejutkan pada awalnya tetapi cukup konsisten.

Tidak akan menjadi masalah untuk hanya menjatuhkan pernyataan jenis yang memungkinkan pada nilai pilihan. Akan sangat disayangkan karena itu membuatnya sangat mudah untuk mempromosikan metode yang dibagikan oleh semua jenis dalam pick tanpa harus menulis semua kasus atau menggunakan refleksi.

Padahal, akan cukup mudah menggunakan pembuatan kode untuk menulis

func (p Pick) String() string {
  switch v := p.[var] {
  case A:
    return v.String()
  case B:
    return v.String()
  //etc
  }
}

Hanya pergi ke depan dan menjatuhkan jenis pernyataan. Mungkin mereka harus ditambahkan tetapi itu bukan bagian penting dari proposal.

Saya ingin kembali ke komentar @ianlancetaylor sebelumnya , karena saya punya beberapa perspektif baru setelah memikirkan lebih lanjut tentang penanganan kesalahan (khususnya, https://github.com/golang/go/issues/21161# issuecomment-320294933).

Secara khusus, apa yang diberikan tipe baru kepada kita yang tidak kita dapatkan dari tipe antarmuka?

Seperti yang saya lihat, keuntungan utama dari tipe jumlah adalah mereka memungkinkan kita untuk membedakan antara mengembalikan beberapa nilai, dan mengembalikan salah satu dari beberapa nilai — terutama ketika salah satu dari nilai tersebut adalah turunan dari antarmuka kesalahan.

Saat ini kami memiliki banyak fungsi formulir

func F(…) (T, error) {
    …
}

Beberapa dari mereka, seperti io.Reader.Read dan io.Reader.Write , mengembalikan T bersama dengan error , sedangkan yang lain kembali baik T atau sebuah error tetapi tidak pernah keduanya. Untuk gaya API sebelumnya, mengabaikan T jika terjadi kesalahan sering kali merupakan bug (misalnya, jika kesalahannya adalah io.EOF ); untuk gaya yang terakhir, mengembalikan T bukan nol adalah bug.

Alat otomatis, termasuk lint , dapat memeriksa penggunaan fungsi tertentu untuk memastikan bahwa nilainya (atau tidak) diabaikan dengan benar ketika kesalahannya bukan nihil, tetapi pemeriksaan tersebut tidak secara alami meluas ke fungsi arbitrer.

Misalnya, proto.Marshal dimaksudkan sebagai gaya "nilai dan kesalahan" jika kesalahannya adalah RequiredNotSetError , tetapi tampaknya gaya "nilai atau kesalahan" sebaliknya. Karena sistem tipe tidak membedakan keduanya, mudah untuk secara tidak sengaja memperkenalkan regresi: baik tidak mengembalikan nilai saat seharusnya, atau mengembalikan nilai saat tidak seharusnya. Dan implementasi proto.Marshaler semakin memperumit masalah.

Di sisi lain, jika kita dapat mengekspresikan tipenya sebagai gabungan, kita bisa lebih eksplisit tentangnya:

type PartialMarshal struct {
    Data []byte // The marshalled value, ignoring unset required fields.
    MissingFields []string
}

func Marshal(pb Message) []byte | PartialMarshal | error

@ianlancetaylor , saya telah bermain-main dengan proposal Anda di atas kertas. Bisakah Anda memberi tahu saya jika ada yang salah di bawah ini?

Diberikan

var r interface{} restrict { uint, int } = 1

tipe dinamis dari r adalah int , dan

var _ interface{} restrict { uint32, int32 } = 1

adalah ilegal.

Diberikan

type R interface{} restrict { struct { n int }, etc }
type S struct { n int }

maka var _ R = S{} akan menjadi ilegal.

Tapi diberikan

type R interface{} restrict { int, error }
type A interface {
  error
  foo()
}
type C struct { error }
func (C) foo() {}

keduanya var _ R = C{} dan var _ R = A(C{}) akan legal.

Keduanya

interface{} restrict { io.Reader, io.Writer }

dan

interface{} restrict { io.Reader, io.Writer, *bytes.Buffer }

setara.

Juga,

interface{} restrict { error, net.Error }

setara dengan

interface { Error() string }

Diberikan

type IO interface{} restrict { io.Reader, io.Writer }
type R interface{} restrict {
  interface{} restrict { int, uint },
  IO,
}

maka tipe dasar dari R setara dengan

interface{} restrict { io.Writer, uint, io.Reader, int }

Sunting: koreksi kecil dalam huruf miring

@jimmyfrasche Saya tidak akan mengatakan bahwa apa yang saya tulis di atas adalah sebuah proposal. Itu lebih seperti sebuah ide. Saya harus memikirkan komentar Anda, tetapi pada pandangan pertama mereka terlihat masuk akal.

Proposal @jimmyfrasche cukup banyak seperti yang saya harapkan secara intuitif dari tipe pick untuk berperilaku di Go. Saya pikir itu sangat penting untuk dicatat bahwa proposalnya untuk menggunakan nilai nol dari bidang pertama untuk nilai nol dari pick adalah intuitif dengan "nilai nol berarti menghilangkan byte", asalkan nilai tag dimulai dari nol (mungkin ini telah dicatat; utas ini sangat panjang sekarang ...). Saya juga menyukai implikasi kinerja (tidak ada alokasi yang tidak perlu), dan pilihan itu sepenuhnya ortogonal dengan antarmuka (tidak ada perilaku mengejutkan yang beralih pada pilihan yang berisi antarmuka).

Satu-satunya hal yang saya pertimbangkan untuk diubah adalah mengubah tag: foo.X = 0 sepertinya bisa foo = Foo{X: 0} ; beberapa karakter lagi, tetapi lebih eksplisit bahwa itu menyetel ulang tag dan memusatkan nilainya. Ini adalah poin kecil, dan saya masih akan sangat senang jika proposalnya diterima apa adanya.

@ns-cweber terima kasih tapi saya tidak bisa menghargai perilaku nilai nol. Ide-ide telah beredar untuk sementara waktu dan ada dalam proposal @rogpeppe yang datang lebih awal di utas ini (seperti yang Anda tunjukkan cukup panjang). Pembenaran saya sama dengan yang Anda berikan.

Sejauh foo.X = 0 vs foo = Foo{X: 0} , proposal saya sebenarnya mengizinkan keduanya. Yang terakhir berguna jika bidang pick itu adalah struct sehingga Anda dapat melakukan foo.X.Y = 0 alih-alih foo = Foo{X: image.Point{X: foo.[X].X, 0}} yang selain verbose bisa gagal saat runtime.

Saya juga berpikir itu membantu untuk tetap seperti itu karena memperkuat elevator pitch untuk semantiknya: Ini adalah struct yang hanya dapat memiliki satu bidang yang ditetapkan pada waktu.

Satu hal yang mungkin menghalanginya untuk diterima apa adanya adalah bagaimana menyematkan pick dalam struct akan bekerja. Saya menyadari beberapa hari yang lalu saya mengabaikan berbagai efek yang akan terjadi pada penggunaan struct. Saya pikir itu bisa diperbaiki tetapi tidak sepenuhnya yakin apa perbaikan terbaik. Yang paling sederhana adalah itu hanya mewarisi metode dan Anda harus langsung merujuk ke pick yang disematkan dengan nama untuk sampai ke bidangnya dan saya condong ke arah itu untuk menghindari struct yang memiliki bidang struct dan bidang pick.

@jimmyfrasche Terima kasih telah mengoreksi saya tentang perilaku bernilai nol. Saya setuju bahwa proposal Anda memungkinkan untuk kedua mutator, dan saya pikir titik pitch elevator Anda bagus. Penjelasan Anda untuk proposal Anda masuk akal, meskipun saya dapat melihat diri saya mengatur foo.XY, tidak menyadari itu akan secara otomatis mengubah bidang pick. Saya masih akan sangat senang jika proposal Anda berhasil, bahkan dengan sedikit keberatan.

Terakhir, proposal sederhana Anda untuk memilih embedding sepertinya yang saya rasakan. Bahkan jika kita berubah pikiran, kita dapat beralih dari proposal sederhana ke proposal kompleks tanpa melanggar kode yang ada, tetapi kebalikannya tidak benar.

@ns-cweber

Saya bisa melihat diri saya mengatur foo.XY, tidak menyadarinya akan secara otomatis mengubah bidang pilihan

Itu poin yang adil, tetapi Anda bisa membahas banyak hal dalam bahasa, atau bahasa apa pun, dalam hal ini. Secara umum, Go memiliki rel pengaman tetapi bukan gunting pengaman.

Ada banyak hal besar yang umumnya melindungi Anda dari, jika Anda tidak berusaha keras untuk menumbangkannya, tetapi Anda masih harus tahu apa yang Anda lakukan.

Itu bisa menjengkelkan ketika Anda membuat kesalahan seperti ini tetapi, otoh, itu tidak jauh berbeda dari "Saya menetapkan bar.X = 0 tetapi saya bermaksud untuk mengatur bar.Y = 0 " karena hipotetis bergantung pada Anda tidak menyadari bahwa foo adalah tipe pick.

Demikian pula i.Foo() , p.Foo() , dan v.Foo() semuanya terlihat sama tetapi jika i adalah antarmuka nil , p adalah pointer nihil dan Foo tidak menangani kasus itu, dua yang pertama bisa panik sedangkan jika v menggunakan penerima metode nilai, itu tidak bisa (setidaknya bukan dari doa itu sendiri) .

Sejauh menyematkan, poin bagus tentangnya mudah dilonggarkan nanti, jadi saya melanjutkan dan mengedit proposal.

Jenis penjumlahan sering kali memiliki bidang yang tidak bernilai. Misalnya, dalam paket database/sql , kita memiliki:

type NullString struct {
    String string
    Valid  bool // Valid is true if String is not NULL
}

Jika kita memiliki tipe sum / picks / unions, ini dapat dinyatakan sebagai:

type NullString pick {
  Null   struct{}
  String string
}

Jenis sum memiliki keunggulan yang jelas dibandingkan struct dalam kasus ini. Saya pikir ini adalah penggunaan yang cukup umum sehingga layak untuk dimasukkan sebagai contoh dalam proposal apa pun.

Bikeshedding (maaf), saya berpendapat ini sepadan dengan dukungan sintaksis dan inkonsistensi dengan sintaks penyematan bidang struct:

type NullString union {
  Null
  String string
}

@neild

Menekan poin terakhir terlebih dahulu: Sebagai perubahan menit terakhir sebelum saya memposting (tidak sepenuhnya diperlukan dalam arti apa pun), saya menambahkan bahwa jika ada tipe bernama (atau penunjuk ke tipe bernama) tanpa nama bidang, pick membuat bidang implisit dengan nama yang sama dengan jenisnya. Itu mungkin bukan ide terbaik tetapi sepertinya itu akan mencakup salah satu kasus umum "salah satu dari jenis ini" tanpa banyak keributan. Mengingat bahwa contoh terakhir Anda dapat ditulis:

type Null = struct{} //though this puts Null in the same scope as NullString
type NullString pick {
  Null
  String string
}

Kembali ke poin utama Anda, ya, itu penggunaan yang sangat baik. Bahkan, Anda dapat menggunakannya untuk membangun enum: type Stoplight pick { Stop, Slow, Go struct{} } . Ini akan seperti const/iota faux-enum. Itu bahkan akan dikompilasi ke output yang sama. Manfaat utama dalam hal ini adalah bahwa nomor yang mewakili keadaan sepenuhnya dienkapsulasi dan Anda tidak dapat memasukkan keadaan apa pun selain dari ketiga yang terdaftar.

Sayangnya, ada sintaks yang agak canggung untuk membuat dan menetapkan nilai Stoplight yang diperparah dalam kasus ini:

light := Stoplight{Slow: struct{}{}}
light.Go = struct{}{}

Mengizinkan {} atau _ menjadi singkatan untuk struct{}{} , seperti yang diusulkan di tempat lain, akan membantu.

Banyak bahasa, terutama bahasa fungsional, menyiasatinya dengan menempatkan label dalam cakupan yang sama dengan jenisnya. Ini menciptakan banyak kerumitan dan akan melarang dua pilihan yang ditentukan dalam cakupan yang sama untuk berbagi nama bidang.

Namun, mudah untuk menyiasatinya dengan pembuat kode yang membuat fungsi dengan nama yang sama untuk setiap bidang dalam pilihan yang menggunakan tipe bidang sebagai argumen. Jika juga, sebagai kasus khusus, tidak mengambil argumen jika tipenya berukuran nol, maka output untuk contoh Stoplight akan terlihat seperti ini

func Stop() Stoplight {
  return Stoplight{Stop: struct{}{}}
}
func Slow() Stoplight {
  return Stoplight{Slow: struct{}{}}
}
func Go() Stoplight {
  return Stoplight{Go: struct{}{}}
}

dan untuk contoh NullString Anda akan terlihat seperti ini:

func Null() NullString {
  return NullString{Null: struct{}{}}
}
func String(s string) NullString {
  return NullString{String: s}
}

Itu tidak cantik, tetapi go generate jauhnya dan kemungkinan sangat mudah digarisbawahi.

Itu tidak akan berfungsi dalam kasus di mana ia membuat bidang implisit berdasarkan nama jenis (kecuali jenisnya berasal dari paket lain) atau dijalankan pada dua pilihan dalam paket yang sama yang berbagi nama bidang, tapi tidak apa-apa. Proposal tidak melakukan segalanya di luar kotak tetapi memungkinkan banyak hal dan memberi programmer fleksibilitas untuk memutuskan apa yang terbaik untuk situasi tertentu.

Lebih banyak pelepasan sepeda sintaks:

type NullString union {
  Null
  Value string
}

var _ = NullString{Null}
var _ = NullString{Value: "some value"}
var _ = NullString{Value} // equivalent to NullString{Value: ""}.

Secara konkret, literal dengan daftar elemen yang tidak berisi kunci ditafsirkan sebagai penamaan bidang yang akan disetel.

Ini secara sintaksis tidak konsisten dengan penggunaan literal komposit lainnya. Di sisi lain, ini adalah penggunaan yang tampaknya masuk akal dan intuitif dalam konteks tipe union/pick/sum (setidaknya bagi saya), karena tidak ada interpretasi yang masuk akal dari penginisialisasi serikat tanpa kunci.

@neild

Ini secara sintaksis tidak konsisten dengan penggunaan literal komposit lainnya.

Itu sepertinya sangat negatif bagi saya, meskipun masuk akal dalam konteksnya.

Perhatikan juga bahwa

var ns NullString // == NullString{Null: struct{}{}} == NullString{}
ns.String = "" // == NullString{String: ""}

Untuk menangani struct{}{} ketika saya menggunakan map[T]struct{} saya melempar

var set struct{}

di suatu tempat dan gunakan theMap[k] = set , Serupa akan bekerja dengan picks

Pelepasan sepeda lebih lanjut: tipe kosong (dalam konteks tipe penjumlahan) secara konvensional dinamai "unit", bukan "null".

@bcmils Semacam .

Dalam bahasa fungsional, saat Anda membuat tipe penjumlahan, labelnya sebenarnya adalah fungsi yang menciptakan nilai dari tipe tersebut, (walaupun fungsi khusus yang dikenal sebagai "konstruktor tipe" atau "tycons" yang diketahui oleh kompiler untuk memungkinkan pencocokan pola), jadi

data Bool = False | True

membuat tipe data Bool dan dua fungsi dalam lingkup yang sama, True dan False , masing-masing dengan tanda tangan () -> Bool .

Di sini () adalah bagaimana Anda menulis tipe yang diucapkan unit—tipe dengan hanya satu nilai. Dalam Go jenis ini dapat ditulis dengan berbagai cara tetapi secara idiomatis ditulis sebagai struct{} .

Jadi jenis argumen konstruktor akan disebut unit. Konvensi untuk nama konstruktor umumnya None bila digunakan sebagai jenis opsi seperti ini, tetapi dapat diubah agar sesuai dengan domain. Null akan menjadi nama yang bagus jika nilainya berasal dari database, misalnya.

@bcmils

Seperti yang saya lihat, keuntungan utama dari tipe jumlah adalah mereka memungkinkan kita untuk membedakan antara mengembalikan beberapa nilai, dan mengembalikan salah satu dari beberapa nilai — terutama ketika salah satu dari nilai tersebut adalah turunan dari antarmuka kesalahan.

Untuk perspektif alternatif, saya melihat ini sebagai kelemahan utama dari tipe jumlah di Go.

Banyak bahasa tentu saja menggunakan tipe sum untuk kasus mengembalikan beberapa nilai atau kesalahan, dan ini bekerja dengan baik untuk mereka. Jika tipe sum ditambahkan ke Go, akan ada godaan besar untuk menggunakannya dengan cara yang sama.

Namun, Go sudah memiliki ekosistem kode yang besar yang menggunakan banyak nilai untuk tujuan ini. Jika kode baru menggunakan tipe jumlah untuk mengembalikan (nilai, kesalahan) tupel, maka ekosistem itu akan menjadi terfragmentasi. Beberapa penulis akan terus menggunakan beberapa pengembalian untuk konsistensi dengan kode yang ada; beberapa penulis akan menggunakan tipe sum; beberapa akan mencoba mengonversi API yang ada. Penulis terjebak pada versi Go yang lebih lama, karena alasan apa pun, akan dikunci dari API baru. Ini akan menjadi berantakan, dan saya tidak berpikir keuntungan akan mulai sepadan dengan biayanya.

Jika kode baru menggunakan tipe jumlah untuk mengembalikan (nilai, kesalahan) tupel, maka ekosistem itu akan menjadi terfragmentasi.

Jika kita menambahkan tipe penjumlahan di Go 2 dan menggunakannya secara seragam, maka masalahnya akan berkurang menjadi salah satu migrasi, bukan fragmentasi: perlu dimungkinkan untuk mengonversi API Go 1 (nilai, kesalahan) ke Go 2 (nilai | kesalahan) ) API dan sebaliknya, tetapi keduanya bisa menjadi tipe yang berbeda di bagian Go 2 dari program.

Jika kita menambahkan tipe penjumlahan di Go 2 dan menggunakannya secara seragam

Perhatikan bahwa ini adalah proposal yang sangat berbeda dari yang terlihat di sini sejauh ini: Pustaka standar perlu difaktorkan ulang secara ekstensif, terjemahan antara gaya API perlu ditentukan, dll. Ikuti rute ini dan ini menjadi cukup besar dan proposal rumit untuk transisi API dengan codicil minor mengenai desain tipe sum.

Tujuannya adalah agar Go 1 dan Go 2 dapat hidup berdampingan dengan mulus dalam proyek yang sama, jadi saya rasa kekhawatirannya adalah bahwa seseorang mungkin terjebak dengan kompiler Go 1 "karena alasan tertentu" dan tidak dapat menggunakan Pergi 2 perpustakaan. Namun, jika Anda memiliki ketergantungan A yang bergantung pada B , dan B memperbarui untuk menggunakan fitur baru seperti pick di API-nya, maka itu akan mematahkan ketergantungan A kecuali jika diperbarui untuk menggunakan versi baru B . A bisa saja vendor B dan tetap menggunakan versi lama, tetapi jika versi lama tidak dipertahankan untuk bug keamanan, dll... atau jika Anda perlu menggunakan versi baru B secara langsung dan Anda tidak dapat memiliki dua versi dalam proyek Anda karena alasan tertentu, yang dapat menimbulkan masalah.

Pada akhirnya, masalahnya di sini tidak ada hubungannya dengan versi bahasa, dan lebih berkaitan dengan mengubah tanda tangan dari fungsi yang diekspor yang ada. Fakta bahwa itu akan menjadi fitur baru yang memberikan dorongan adalah sedikit gangguan dari itu. Jika tujuannya adalah untuk mengizinkan API yang ada diubah untuk menggunakan pick tanpa merusak kompatibilitas mundur, maka mungkin perlu ada semacam sintaks jembatan. Misalnya (sepenuhnya sebagai manusia jerami):

type ReadResult pick(N int, Err error) {
    N
    PartialResult struct { N; Err }
    Err
}

Kompiler bisa saja memercikkan ReadResult ketika diakses oleh kode lama, menggunakan nilai nol jika bidang tidak ada dalam varian tertentu. Saya tidak yakin bagaimana pergi ke arah lain atau apakah itu sepadan. API seperti template.Must mungkin hanya harus terus menerima beberapa nilai daripada pick dan mengandalkan percikan untuk membuat perbedaan. Atau sesuatu seperti ini dapat digunakan:

type ReadResult pick(N int, Err error) {
case Err == nil:
    N
default:
    PartialResult struct { N; Err }
case N == 0:
    Err
}

Ini memang memperumit banyak hal, tetapi saya dapat melihat bagaimana memperkenalkan fitur yang mengubah cara API harus ditulis membutuhkan cerita tentang cara bertransisi tanpa merusak dunia. Mungkin ada cara untuk melakukannya yang tidak memerlukan sintaks jembatan.

Sangat sepele untuk beralih dari tipe sum ke tipe produk (struct, beberapa nilai pengembalian) — cukup atur semua yang bukan nilainya ke nol. Pergi dari jenis produk ke jenis jumlah tidak didefinisikan dengan baik secara umum.

Jika API ingin bertransisi secara mulus dan bertahap dari implementasi berbasis tipe produk ke tipe berbasis jumlah, rute termudah adalah memiliki dua versi dari semua yang diperlukan di mana versi tipe jumlah memiliki implementasi aktual dan versi tipe produk memanggil versi tipe sum, melakukan pemeriksaan runtime apa pun yang diperlukan dan proyeksi apa pun ke dalam ruang produk.

Itu sangat abstrak jadi inilah contohnya

versi 1 tanpa jumlah

func Take(i interface{}) error {
  switch i.(type) {
  case int: //do something
  case string:
  default: return fmt.Errorf("invalid %T", i)
  }
}
func Give() (interface{}, error) {
   i := f() //something
   if i == nil {
     return nil, errors.New("whoops v:)v")
  }
  return i
}

versi 2 dengan jumlah

type Value pick {
  I int
  S string
}
func TakeSum(v Value) {
  // do something
}
// Deprecated: use TakeSum
func Take(i interface{}) error {
  switch x := i.(type) {
  case int: TakeSum(Value{I: x})
  case string: TakeSum(Value{S: x})
  default: return fmt.Errorf("invalid %T", i)
  }
}
type ErrValue pick {
  Value
  Err error
}
func GiveSum() ErrValue { //though honestly (Value, error) is fine
  return f()
}
// Deprecated: use GiveSum
func Give() (interface{}, error) {
  switch v := GiveSum().(var) {
  case Value:
    switch v := v.(var) {
    case I: return v, nil
    case S: return v, nil
    }
  case Err:
    return nil, v
  }
}

versi 3 akan menghapus Beri/Ambil

versi 4 akan memindahkan implementasi GiveSum/TakeSum ke Give/Take, buat GiveSum/TakeSum panggil saja Give/Take dan deprecate GiveSum/TakeSum.

versi 5 akan menghapus GiveSum/TakeSum

Ini tidak cantik atau cepat tetapi sama seperti gangguan skala besar lainnya yang serupa dan tidak memerlukan tambahan bahasa

Saya pikir (sebagian besar) utilitas tipe sum dapat direalisasikan dengan mekanisme untuk membatasi penetapan ke tipe antarmuka tipe{} pada waktu kompilasi.

Dalam mimpi saya itu terlihat seperti:

type T1 switch {T2,T3} // only nil, T2 and T3 may be assigned to T1
type T2 struct{}
type U switch {} // only nil may be assigned to U
type V switch{interface{} /* ,... */} // V equivalent to interface{}
type Invalid switch {T2,T2} // only uniquely named types
type T3 switch {int,uint} // switches can contain switches but... 

... itu juga akan menjadi kesalahan waktu kompilasi untuk menegaskan tipe sakelar adalah tipe yang tidak didefinisikan secara eksplisit:

var t1 T1
i,ok := t1.(int) // T1 can't be int, only T2 or T3 (but T3 could be int)
switch t := t1.(type) {
    case int: // compile error, T1 is just nil, T2 or T3
}

dan pergi dokter hewan akan mencari tahu tentang penugasan konstan yang ambigu untuk tipe seperti T3 tetapi untuk semua maksud dan tujuan (saat runtime) var x T3 = 32 akan menjadi var x interface{} = 32 . Mungkin beberapa jenis sakelar yang telah ditentukan sebelumnya untuk bawaan dalam sebuah paket bernama sesuatu seperti sakelar atau kuda poni juga akan menyenangkan.

@j7b , @ianlancetaylor menawarkan ide serupa di https://github.com/golang/go/issues/19412#issuecomment -323256891

Saya memposting apa yang saya yakini sebagai konsekuensi logis dari ini nanti di https://github.com/golang/go/issues/19412#issuecomment -325048452

Sepertinya banyak dari mereka akan berlaku sama mengingat kesamaannya.

Akan sangat bagus jika sesuatu seperti itu akan berhasil. Akan mudah untuk bertransisi dari antarmuka ke antarmuka+pembatasan (terutama dengan sintaks Ian: cukup tempelkan restrict di akhir jumlah semu yang ada yang dibuat dengan antarmuka). Akan mudah diimplementasikan karena pada saat runtime mereka pada dasarnya identik dengan antarmuka dan sebagian besar pekerjaan hanya akan membuat kompiler mengeluarkan kesalahan tambahan ketika invarian mereka rusak.

Tapi saya tidak berpikir itu mungkin untuk membuatnya bekerja.

Semuanya berbaris begitu dekat sehingga terlihat seperti cocok, tapi Anda memperbesar dan itu hanya tidak benar, sehingga Anda memberikan sedikit dorongan dan kemudian sesuatu yang muncul yang lain tidak sejajar. Anda dapat mencoba memperbaikinya tetapi kemudian Anda mendapatkan sesuatu yang sangat mirip dengan antarmuka tetapi berperilaku berbeda dalam kasus-kasus aneh.

Mungkin aku kehilangan sesuatu.

Tidak ada yang salah dengan proposal antarmuka terbatas selama Anda setuju dengan kasing tidak harus terputus-putus. Saya tidak berpikir itu mengejutkan seperti yang Anda lakukan bahwa penyatuan antara dua jenis antarmuka (seperti io.Reader / io.Writer ) tidak terputus-putus. Ini sepenuhnya konsisten dengan fakta bahwa Anda tidak dapat menentukan apakah nilai yang ditetapkan ke interface{} telah disimpan sebagai io.Reader atau io.Writer jika mengimplementasikan keduanya. Fakta bahwa Anda dapat membangun serikat yang terputus-putus selama setiap kasus adalah tipe konkret tampaknya sangat memadai.

Imbalannya adalah, jika serikat pekerja adalah antarmuka yang dibatasi, maka Anda tidak dapat mendefinisikan metode secara langsung pada mereka. Dan jika jenis antarmukanya dibatasi, Anda tidak mendapatkan jaminan penyimpanan langsung yang disediakan oleh jenis pick . Apakah perlu menambahkan jenis hal yang berbeda ke bahasa untuk mendapatkan manfaat tambahan ini, saya tidak yakin.

@jimmyfrasche untuk type T switch {io.Reader,io.Writer} boleh saja menetapkan ReadWriter ke T tetapi Anda hanya dapat menyatakan T adalah io.Reader atau Io.Writer, Anda perlu pernyataan lain untuk menegaskan io.Reader atau io.Writer adalah a ReadWriter, yang seharusnya mendorong untuk menambahkannya ke switchtype jika itu adalah pernyataan yang berguna.

@stevenblenkinsop Anda dapat menentukan proposal pemilihan tanpa metode. Bahkan, jika Anda menyingkirkan metode dan nama bidang implisit, Anda dapat mengizinkan penyematan pick. (Meskipun jelas saya pikir metode dan, pada tingkat yang jauh lebih rendah, nama bidang implisit, adalah perdagangan yang lebih berguna di sana).

Dan, di sisi lain, sintaks @ianlancetaylor akan memungkinkan

type IR interface {
  Foo()
  Bar()
} restrict { A, B, C }

yang akan dikompilasi selama A , B , dan C masing-masing memiliki Foo dan Bar metode (meskipun Anda harus khawatir tentang nilai nil ).

edit: klarifikasi dalam huruf miring

Saya pikir beberapa bentuk _restricted interface_ akan berguna, tetapi saya tidak setuju dengan sintaksnya. Inilah yang saya sarankan. Ini bertindak dengan cara yang sama seperti tipe data aljabar, yang mengelompokkan objek terkait domain yang tidak harus memiliki perilaku yang sama.

//MyGroup can be any of these. It can contain other groups, interfaces, structs, or primitive types
type MyGroup group {
   MyOtherGroup
   MyInterface
   MyStruct
   int
   string
   //..possibly some other types as well
}

//type definitions..
type MyInterface interface{}
type MyStruct struct{}
//etc..

func DoWork(item MyGroup) {
   switch t:=item.(type) {
      //do work here..
   }
}

Ada beberapa manfaat dari pendekatan ini dibandingkan pendekatan antarmuka kosong konvensional interface{} :

  • pemeriksaan tipe statis saat fungsi digunakan
  • pengguna dapat menyimpulkan jenis argumen apa yang diperlukan dari tanda tangan fungsi saja, tanpa harus melihat implementasi fungsi

Antarmuka kosong interface{} berguna ketika jumlah tipe yang terlibat tidak diketahui. Anda benar-benar tidak punya pilihan di sini selain mengandalkan verifikasi runtime. Di sisi lain, ketika jumlah jenis terbatas dan diketahui selama waktu kompilasi, mengapa tidak meminta kompiler untuk membantu kami?

@henryas Saya pikir perbandingan yang lebih berguna adalah cara yang saat ini direkomendasikan untuk melakukan (membuka) jenis penjumlahan: Antarmuka yang tidak kosong (jika tidak ada antarmuka yang jelas dapat disaring, menggunakan fungsi penanda yang tidak diekspor).
Saya tidak berpikir argumen Anda berlaku untuk itu secara signifikan.

Berikut adalah laporan pengalaman terkait Go protobufs:

  • Sintaks proto2 memungkinkan untuk bidang "opsional", yang merupakan tipe di mana ada perbedaan antara nilai nol dan nilai yang tidak disetel. Solusi saat ini adalah dengan menggunakan pointer (misalnya, *int ), di mana pointer nil menunjukkan tidak disetel, sedangkan pointer yang disetel menunjuk ke nilai aktual. Keinginan adalah pendekatan yang memungkinkan membuat perbedaan antara nol dan tidak disetel menjadi mungkin, tanpa memperumit kasus umum yang hanya perlu mengakses nilai (di mana nilai nol baik-baik saja jika tidak disetel).

    • Ini tidak berkinerja karena alokasi ekstra (walaupun serikat pekerja mungkin mengalami nasib yang sama tergantung pada implementasinya).
    • Ini menyakitkan bagi pengguna karena kebutuhan untuk terus-menerus memeriksa penunjuk mengganggu keterbacaan (walaupun nilai default bukan nol dalam protos mungkin berarti kebutuhan untuk memeriksa adalah hal yang baik ...).
  • Bahasa proto memungkinkan untuk "one ofs", yang merupakan versi proto dari tipe sum. Pendekatan yang dilakukan saat ini adalah sebagai berikut ( contoh kasar ):

    • Tentukan tipe antarmuka dengan metode tersembunyi (misalnya, type Communique_Union interface { isCommunique_Union() } )
    • Untuk setiap kemungkinan tipe Go yang diizinkan dalam serikat, tentukan struct pembungkus, yang tujuannya hanya untuk membungkus setiap tipe yang diizinkan (misalnya, type Communique_Number struct { Number int32 } ) di mana setiap tipe memiliki metode isCommunique_Union .
    • Ini juga tidak berfungsi karena pembungkus menyebabkan alokasi. Jenis jumlah akan membantu karena kita tahu bahwa nilai terbesar (sepotong) akan menempati tidak lebih dari 24B.

@henryas Saya pikir perbandingan yang lebih berguna adalah cara yang saat ini direkomendasikan untuk melakukan (membuka) jenis penjumlahan: Antarmuka yang tidak kosong (jika tidak ada antarmuka yang jelas dapat disaring, menggunakan fungsi penanda yang tidak diekspor).
Saya tidak berpikir argumen Anda berlaku untuk itu secara signifikan.

Maksud Anda dengan menambahkan metode dummy unexported ke suatu objek sehingga objek tersebut dapat dilewatkan sebagai antarmuka, sebagai berikut?

type MyInterface interface {
   belongToMyInterface() //dummy method definition
}

type MyObject struct{}
func (MyObject) belongToMyInterface(){} //dummy method

Saya tidak berpikir itu harus direkomendasikan sama sekali. Ini lebih seperti solusi daripada solusi. Saya pribadi lebih suka melupakan verifikasi tipe statis daripada memiliki metode kosong dan definisi metode yang tidak perlu tergeletak di sekitar.

Ini adalah masalah dengan pendekatan _dummy method_:

  • Metode dan definisi metode yang tidak perlu mengacaukan objek dan antarmuka.
  • Setiap kali _group_ baru ditambahkan, Anda perlu memodifikasi implementasi objek (mis. menambahkan metode dummy). Ini salah (lihat poin berikutnya).
  • Tipe data aljabar (atau pengelompokan berdasarkan _domain_ daripada perilaku) adalah spesifik domain . Bergantung pada domain, Anda mungkin perlu melihat hubungan objek secara berbeda. Akuntan mengelompokkan dokumen secara berbeda dari manajer gudang. Pengelompokan ini menyangkut konsumen objek, dan bukan objek itu sendiri. Objek tidak perlu tahu apa-apa tentang masalah konsumen, dan seharusnya tidak perlu. Apakah Faktur perlu tahu apa-apa tentang akuntansi? Jika tidak, mengapa Faktur perlu mengubah implementasinya _(mis. menambahkan metode dummy baru)_ setiap kali ada perubahan dalam aturan akuntansi _(mis. menerapkan pengelompokan dokumen baru)_? Dengan menggunakan pendekatan _dummy method_, Anda menggabungkan objek Anda ke domain konsumen dan membuat asumsi yang signifikan tentang domain konsumen. Anda tidak perlu melakukan ini. Ini bahkan lebih buruk daripada pendekatan antarmuka kosong interface{} . Ada pendekatan yang lebih baik yang tersedia.

@henryas

Saya tidak melihat poin ketiga Anda sebagai argumen yang kuat. Jika akuntan ingin melihat hubungan objek secara berbeda, akuntan dapat membuat antarmuka mereka sendiri yang sesuai dengan spesifikasi mereka. Menambahkan metode pribadi ke antarmuka tidak berarti tipe konkret yang memenuhinya tidak kompatibel dengan subset antarmuka yang ditentukan di tempat lain.

Parser Go banyak menggunakan teknik ini dan sejujurnya saya tidak dapat membayangkan picks membuat paket itu jauh lebih baik sehingga memerlukan implementasi picks dalam bahasa tersebut.

@as Maksud saya adalah bahwa setiap kali _relationship view_ baru dibuat, objek konkret yang relevan harus diperbarui untuk membuat akomodasi tertentu untuk tampilan ini. Tampaknya salah, karena untuk melakukan itu, objek harus sering membuat asumsi tertentu tentang domain konsumen. Jika objek dan konsumen terkait erat atau hidup dalam domain yang sama, seperti dalam kasus parser Go, itu mungkin tidak terlalu menjadi masalah. Namun, jika objek menyediakan fungsionalitas dasar yang akan dikonsumsi oleh beberapa domain lain, itu menjadi masalah. Objek sekarang perlu tahu sedikit tentang semua domain lain agar pendekatan _dummy method_ berfungsi.

Anda berakhir dengan banyak metode kosong yang dilampirkan ke objek, dan tidak jelas bagi pembaca mengapa Anda memerlukan metode tersebut karena antarmuka yang mengharuskannya tinggal di domain/paket/lapisan terpisah.

Poin bahwa pendekatan open-sum-via-interfaces tidak memungkinkan Anda menggunakan jumlah dengan mudah sudah cukup adil. Jenis sum eksplisit jelas akan membuat penjumlahan lebih mudah. Ini adalah argumen yang sangat berbeda dari "jumlah tipe memberi Anda keamanan tipe", - Anda masih bisa mendapatkan keamanan tipe hari ini, jika Anda membutuhkannya.

Saya masih melihat dua kelemahan dari penjumlahan tertutup seperti yang diterapkan dalam bahasa lain: Satu, kesulitan mengembangkannya dalam proses pengembangan terdistribusi skala besar. Dan Kedua, saya pikir mereka menambahkan kekuatan ke sistem tipe dan saya suka bahwa Go tidak memiliki sistem tipe yang sangat kuat, karena hal itu menghambat tipe pengkodean dan alih-alih mengkode program - ketika saya merasa bahwa suatu masalah dapat mengambil manfaat dari a sistem tipe yang lebih kuat, saya pindah ke bahasa yang lebih kuat (seperti Haskell atau Rust).

Yang sedang berkata, setidaknya yang kedua jelas merupakan salah satu preferensi dan bahkan jika Anda setuju, apakah kerugiannya dianggap lebih besar daripada keuntungannya juga tergantung pada preferensi pribadi. Hanya ingin menunjukkan, bahwa Anda tidak bisa mendapatkan jumlah yang aman tanpa jenis jumlah tertutup tidak benar :)

[1] khususnya, itu tidak mudah, tetapi masih mungkin , misalnya Anda bisa melakukannya

type Node interface {
    node()
}

type Foo struct {
    bar.Baz
}

func (foo) node() {}

@Merovius
Saya tidak setuju dengan titik kelemahan kedua Anda. Fakta bahwa ada banyak tempat di perpustakaan standar yang akan sangat diuntungkan dari tipe jumlah, tetapi sekarang diimplementasikan menggunakan antarmuka kosong dan panik, menunjukkan bahwa kekurangan ini merugikan pengkodean. Tentu saja, orang mungkin mengatakan bahwa karena kode tersebut telah ditulis sejak awal, tidak ada masalah dan kita tidak memerlukan tipe penjumlahan, tetapi kebodohan logika itu adalah kita tidak memerlukan tipe lain untuk fungsi tanda tangan, dan kita sebaiknya menggunakan antarmuka kosong saja.

Adapun menggunakan antarmuka dengan beberapa metode untuk mewakili tipe penjumlahan sekarang, ada satu kelemahan besar. Anda tidak tahu tipe apa yang dapat Anda gunakan untuk antarmuka itu, karena mereka diimplementasikan secara implisit. Dengan tipe sum yang tepat, tipe itu sendiri menggambarkan dengan tepat tipe apa yang sebenarnya dapat digunakan.

Saya tidak setuju dengan titik kelemahan kedua Anda.

Apakah Anda tidak setuju dengan pernyataan "jumlah tipe mendorong pemrograman dengan tipe", atau apakah Anda tidak setuju dengan itu sebagai sisi negatifnya? Karena sepertinya Anda tidak setuju dengan yang pertama (komentar Anda pada dasarnya hanya penegasan ulang) dan mengenai yang kedua, saya mengakui bahwa itu terserah preferensi di atas.

Fakta bahwa ada banyak tempat di perpustakaan standar yang akan sangat diuntungkan dari tipe jumlah, tetapi sekarang diimplementasikan menggunakan antarmuka kosong dan panik, menunjukkan bahwa kekurangan ini merugikan pengkodean. Tentu saja, orang mungkin mengatakan bahwa karena kode tersebut telah ditulis sejak awal, tidak ada masalah dan kita tidak memerlukan tipe penjumlahan, tetapi kebodohan logika itu adalah kita tidak memerlukan tipe lain untuk fungsi tanda tangan, dan kita sebaiknya menggunakan antarmuka kosong saja.

Jenis argumen hitam-putih ini tidak terlalu membantu . Saya setuju, bahwa jumlah jenis akan mengurangi rasa sakit dalam beberapa kasus. Setiap perubahan yang membuat sistem tipe lebih kuat akan mengurangi rasa sakit dalam beberapa kasus - tetapi juga akan menyebabkan rasa sakit dalam beberapa kasus. Jadi pertanyaannya adalah, mana yang lebih besar dari yang lain (dan itu, pada tingkat yang baik, pertanyaan preferensi).

Diskusi seharusnya tidak tentang apakah kita menginginkan sistem tipe python-esque (tanpa tipe) atau sistem tipe coq-esque (bukti kebenaran untuk semuanya). Diskusi harus "melakukan manfaat dari jenis jumlah lebih besar daripada kerugiannya" dan sangat membantu untuk mengakui keduanya.


FTR, saya ingin menekankan kembali bahwa, secara pribadi, saya tidak akan menentang tipe open sum (yaitu setiap tipe sum memiliki kasus "SomethingElse" implisit atau eksplisit), karena akan mengurangi sebagian besar kerugian teknis dari mereka (kebanyakan bahwa mereka sulit untuk berkembang) sementara juga menyediakan sebagian besar keuntungan teknis dari mereka (pemeriksaan tipe statis, dokumentasi yang Anda sebutkan, Anda dapat menghitung jenis dari paket lain ...).

Saya juga berasumsi, bahwa jumlah terbuka a) tidak akan menjadi kompromi yang memuaskan bagi orang-orang yang biasanya mendorong jenis jumlah dan b) mungkin tidak akan dianggap sebagai manfaat yang cukup besar untuk menjamin penyertaan oleh tim Go. Tapi saya siap dibuktikan salah pada salah satu atau kedua asumsi ini :)

Satu pertanyaan lagi:

Fakta bahwa ada banyak tempat di perpustakaan standar yang akan sangat diuntungkan dari tipe sum

Saya hanya bisa memikirkan dua tempat di perpustakaan standar, di mana saya akan mengatakan ada manfaat yang signifikan bagi mereka: mencerminkan dan pergi/ast. Dan bahkan di sana, paket-paket itu tampaknya berfungsi dengan baik tanpanya. Dari titik referensi ini, kata "banyak" dan "sangat" tampak berlebihan - tetapi saya mungkin tidak melihat banyak tempat yang sah, tentu saja.

database/sql/driver.Value mungkin mendapat manfaat dari menjadi tipe jumlah (seperti yang disebutkan di #23077).
https://godoc.corp.google.com/pkg/database/sql/driver#Value

Antarmuka yang lebih publik di database/sql.Rows.Scan tidak akan, bagaimanapun, tanpa kehilangan fungsionalitas. Pemindaian dapat membaca nilai-nilai yang tipe dasarnya adalah misalnya, int ; mengubah parameter tujuannya ke tipe jumlah akan membutuhkan pembatasan inputnya ke kumpulan tipe yang terbatas.
https://godoc.corp.google.com/pkg/database/sql#Rows.Scan

@Merovius

Saya tidak akan menentang tipe open sum (yaitu setiap tipe sum memiliki kasus "SomethingElse" implisit atau eksplisit), karena itu akan mengurangi sebagian besar kerugian teknis dari mereka (kebanyakan bahwa mereka sulit untuk berkembang)

Setidaknya ada dua opsi lain yang meringankan masalah "sulit berkembang" dari jumlah tertutup.

Salah satunya adalah mengizinkan kecocokan pada tipe yang sebenarnya bukan bagian dari penjumlahan. Kemudian, untuk menambahkan anggota ke jumlah tersebut, Anda terlebih dahulu memperbarui konsumennya agar sesuai dengan anggota baru, dan hanya benar-benar menambahkan anggota itu setelah konsumen diperbarui.

Cara lainnya adalah mengizinkan anggota yang “tidak mungkin”: yaitu, anggota yang secara eksplisit diizinkan dalam kecocokan tetapi secara eksplisit tidak diizinkan dalam nilai aktual. Untuk menambahkan anggota ke jumlah, pertama-tama Anda menambahkannya sebagai anggota yang tidak mungkin, kemudian memperbarui konsumen, dan akhirnya mengubah anggota baru menjadi mungkin.

database/sql/driver.Value mungkin mendapat manfaat dari menjadi tipe jumlah

Setuju, tidak tahu tentang yang itu. Terima kasih :)

Salah satunya adalah mengizinkan kecocokan pada tipe yang sebenarnya bukan bagian dari penjumlahan. Kemudian, untuk menambahkan anggota ke jumlah tersebut, Anda terlebih dahulu memperbarui konsumennya agar sesuai dengan anggota baru, dan hanya benar-benar menambahkan anggota itu setelah konsumen diperbarui.

Solusi yang menarik.

Antarmuka @Merovius pada dasarnya adalah keluarga tipe jumlah tak terbatas. Semua tipe penjumlahan, tak terbatas atau lainnya, memiliki kasus default: . Namun, tanpa tipe jumlah terbatas, default: berarti kasus valid yang tidak Anda ketahui atau kasus tidak valid yang merupakan bug di suatu tempat dalam program—dengan jumlah terbatas hanya kasus yang pertama dan tidak pernah yang terakhir.

json.Token dan tipe sql.Null* adalah contoh kanonik lainnya. go/types akan mendapat manfaat yang sama dengan go/ast. Saya menduga ada banyak contoh yang tidak ada di API yang diekspor di mana akan lebih mudah untuk men-debug dan menguji beberapa pipa rumit dengan membatasi domain dari keadaan internal. Saya menemukan mereka paling berguna untuk keadaan internal dan kendala aplikasi yang tidak sering muncul di API publik untuk perpustakaan umum, meskipun mereka kadang-kadang digunakan di sana juga.

Secara pribadi saya pikir tipe sum memberi Go kekuatan ekstra yang cukup tetapi tidak terlalu banyak. Sistem tipe Go sudah sangat bagus dan fleksibel, meskipun memiliki kekurangan. Penambahan Go2 ke sistem tipe tidak akan menghasilkan daya sebanyak yang sudah ada—80-90% dari yang dibutuhkan sudah tersedia. Maksud saya, bahkan obat generik pada dasarnya tidak akan membiarkan Anda melakukan sesuatu yang baru: itu akan membiarkan Anda melakukan hal-hal yang sudah Anda lakukan dengan lebih aman, lebih mudah, lebih baik, dan dengan cara yang memungkinkan perkakas yang lebih baik. Jenis jumlah serupa, imo (meskipun jelas jika itu adalah satu atau obat generik lainnya akan diutamakan (dan mereka berpasangan dengan baik)).

Jika Anda mengizinkan default asing (semua kasus + default diizinkan) pada sakelar tipe-jumlah dan tidak memiliki kompiler yang memaksakan kelengkapan (meskipun linter bisa), menambahkan kasing ke jumlah sama mudahnya (dan sama sulitnya ) sebagai mengubah API publik lainnya.

json.Token dan tipe sql.Null* adalah contoh kanonik lainnya.

Token - tentu saja. Contoh lain dari masalah AST (pada dasarnya semua parser mendapat manfaat dari tipe jumlah).

Saya tidak melihat manfaat untuk sql.Null*. Tanpa generik (atau menambahkan beberapa builtin opsional generik "ajaib"), Anda masih harus memiliki tipe dan sepertinya tidak ada perbedaan yang signifikan antara type NullBool enum { Invalid struct{}; Value Int } dan type NullBool struct { Valid bool; Value Int } . Ya, saya sadar ada perbedaan, tapi itu semakin kecil.

Jika Anda mengizinkan default asing (semua kasus + default diizinkan) pada sakelar tipe-jumlah dan tidak memiliki kompiler yang memaksakan kelengkapan (meskipun linter bisa), menambahkan kasing ke jumlah sama mudahnya (dan sama sulitnya ) sebagai mengubah API publik lainnya.

Lihat di atas. Itu yang saya sebut open sum, saya kurang menentangnya.

Itu yang saya sebut open sum, saya kurang menentangnya.

Proposal spesifik saya adalah https://github.com/golang/go/issues/19412#issuecomment -323208336 dan saya yakin ini dapat memenuhi definisi Anda tentang terbuka, meskipun masih agak kasar dan saya yakin masih ada lagi hapus dan poles. Secara khusus saya perhatikan tidak jelas bahwa kasing default dapat diterima meskipun semua kasing terdaftar, jadi saya baru saja memperbaruinya.

Setuju bahwa tipe opsional bukanlah aplikasi pembunuh tipe penjumlahan. Mereka cukup bagus dan seperti yang Anda tunjukkan dengan obat generik yang mendefinisikan a

type Nullable(T) pick { // or whatever syntax (on all counts)
  Null struct{}
  Value T
}

sekali dan mencakup semua kasus akan bagus. Tapi, seperti yang Anda juga tunjukkan, kita bisa melakukan hal yang sama dengan produk generik (struct). Ada status valid dari Valid = false, Value != 0. Dalam skenario itu akan mudah untuk melakukan rooting jika itu menyebabkan masalah karena 2 T kecil, meskipun tidak sekecil 1 + T.

Tentu saja jika itu adalah jumlah yang lebih rumit dengan banyak kasus dan banyak invarian yang tumpang tindih, menjadi lebih mudah untuk membuat kesalahan dan lebih sulit untuk menemukan kesalahan bahkan dengan pemrograman defensif, jadi membuat hal-hal yang tidak mungkin tidak dikompilasi sama sekali dapat menghemat banyak rambut menarik.

Token - tentu saja. Contoh lain dari masalah AST (pada dasarnya semua parser mendapat manfaat dari tipe jumlah).

Saya menulis banyak program yang mengambil beberapa input, melakukan beberapa pemrosesan, dan menghasilkan beberapa output dan saya biasanya membagi ini secara rekursif menjadi banyak lintasan yang membagi input menjadi beberapa kasus dan mengubahnya berdasarkan kasus-kasus itu sebagai langkah yang semakin dekat ke keluaran yang diinginkan. Saya mungkin tidak benar-benar menulis parser (diakui kadang-kadang saya melakukannya karena itu menyenangkan!) tetapi saya menemukan masalah AST, seperti yang Anda katakan, berlaku untuk banyak kode — terutama ketika berhadapan dengan logika bisnis muskil yang memiliki terlalu banyak hal aneh. persyaratan dan kasus tepi agar sesuai di kepala kecil saya.

Saat saya menulis perpustakaan umum, itu tidak muncul di API sesering melakukan beberapa ETL atau membuat beberapa laporan aneh atau memastikan bahwa pengguna di negara bagian X memiliki tindakan Y terjadi jika mereka tidak ditandai Z. Bahkan di perpustakaan umum meskipun saya menemukan tempat di mana dapat membatasi keadaan internal akan membantu, bahkan jika itu hanya mengurangi debug 10 menit menjadi 1 detik "oh kompiler mengatakan saya salah".

Dengan Go di satu tempat di mana saya akan menggunakan tipe sum adalah goroutine yang memilih banyak saluran di mana saya perlu memberikan 3 saluran ke satu goroutine dan 2 ke yang lain. Ini akan membantu saya melacak apa yang terjadi untuk dapat menggunakan chan pick { a A; b B; c C } lebih dari chan A , chan B , chan C meskipun chan stuct { kind MsgKind; a A; b B; c C } dapat lakukan pekerjaan dalam keadaan darurat dengan biaya ruang ekstra dan validasi yang lebih sedikit.

Alih-alih tipe baru, bagaimana dengan pemeriksaan daftar tipe waktu kompilasi sebagai tambahan untuk fitur sakelar tipe antarmuka yang ada?

func main() {
    if FlipCoin() == false {
        printCertainTypes(FlipCoin(), int(5))
    } else {
        printCertainTypes(FlipCoin(), string("5"))
    }
}
// this function compiles with main
func printCertainTypes(flip bool, in interface{}) {
    if flip == false {
        switch v := in.(type) {
        case int:
            fmt.Printf(“integer %v\n”, v)
        default:
            fmt.Println(v)
        }
    } else {
        switch v := in.(type) {
        case int:
            fmt.Printf(“integer %v\n”, v)
        case string:
            fmt.Printf(“string %v\n”, v)
        }
    }
}
// this function compiles with main
func printCertainTypes(flip bool, in interface{}) {
    switch v := in.(type) {
    case int:
        fmt.Printf(“integer %v\n”, v)   
    case bool:
        fmt.Printf(“bool %v\n”, v)
    }
    fmt.Println(flip)
    switch v := in.(type) {
    case string:
        fmt.Printf(“string %v\n”, v)
    case bool:
        fmt.Printf(“bool 2 %v\n”, v)
    }
}
// this function emits a type switch not complete error when compiled with main
func printCertainTypes(flip bool, in interface{}) {
    if flip == false {
        switch v := in.(type) {
        case int:
            fmt.Printf(“integer %v\n”, v)
        case bool:
            fmt.Printf(“bool %v\n”, v)
        }
    } else {
        switch v := in.(type) {
        case string:
            fmt.Printf(“string %v\n”, v)
        case bool:
            fmt.Printf(“bool %v\n”, v)
        }
    }
}
// this function emits a type switch not complete error when compiled with main
func printCertainTypes(flip bool, in interface{}) {
    fmt.Println(flip)
    switch v := in.(type) {
    case int:
        fmt.Printf(“integer %v\n”, v)
    case bool:
        fmt.Printf(“bool %v\n”, v)
    }
}

Dalam keadilan, kita harus mencari cara untuk memperkirakan jumlah tipe dalam sistem tipe saat ini dan mempertimbangkan pro dan kontranya. Jika tidak ada yang lain, itu memberikan dasar untuk perbandingan.

Sarana standar adalah antarmuka dengan metode yang tidak diekspor dan tidak melakukan apa-apa sebagai tag.

Satu argumen menentang ini adalah bahwa setiap jenis dalam jumlah harus memiliki tag ini yang ditentukan di atasnya. Ini tidak sepenuhnya benar, setidaknya untuk anggota yang merupakan struct, kita bisa melakukannya

type Sum interface { sum() }
type sum struct{}
func (sum) sum() {}

dan cukup sematkan tag 0-width itu di struct kita.

Kami dapat menambahkan tipe eksternal ke jumlah kami dengan memperkenalkan pembungkus

type External struct {
  sum
  *pkg.SomeType
}

meskipun ini agak canggung.

Jika semua anggota dalam penjumlahan memiliki perilaku yang sama, kita dapat menyertakan metode tersebut dalam definisi antarmuka.

Konstruksi seperti ini mari kita katakan bahwa suatu tipe ada dalam jumlah, tetapi tidak membiarkan kita mengatakan apa yang tidak ada dalam jumlah itu. Selain kasus nil wajib, trik penyematan yang sama dapat digunakan oleh paket eksternal seperti

import "p"
var member struct {
  p.Sum
}

Di dalam paket kita harus berhati-hati untuk memvalidasi nilai yang dikompilasi tetapi ilegal.

Ada berbagai cara untuk memulihkan beberapa jenis-keamanan saat runtime. Saya telah menemukan termasuk metode valid() error dalam definisi antarmuka jumlah yang digabungkan dengan fungsi seperti

func valid(s Sum) error {
  switch s.(type) {
  case nil:
    return errors.New("pkg: Sum must be non-nil")
  case A, B, C, ...: // listing each valid member
    return s.valid()
  }
  return fmt.Errorf("pkg: %T is not a valid member of Sum")
}

berguna karena memungkinkan menangani dua jenis validasi sekaligus. Untuk anggota yang kebetulan selalu valid, kami dapat menghindari beberapa boilerplate dengan

type alwaysValid struct{}
func (alwaysValid) valid() error { return nil }

Salah satu keluhan yang lebih umum tentang pola ini adalah bahwa hal itu tidak membuat keanggotaan dalam jumlah jelas di godoc. Karena itu juga tidak mengizinkan kami mengecualikan anggota dan tetap mengharuskan kami untuk memvalidasi, ada cara sederhana untuk mengatasi ini: ekspor metode dummy.
Dari pada,

//A Node is one of (list of types).
type Node interface { node() }

menulis

//A Node is only valid if it is defined in this package.
type Node interface { 
  //Node is a dummy method that signifies that a type is a Node.
  Node()
}

Kami tidak dapat menghentikan siapa pun untuk memuaskan Node jadi sebaiknya kami memberi tahu mereka apa yang berhasil. Meskipun ini tidak memperjelas secara sekilas jenis mana yang memenuhi Node (tidak ada daftar pusat), hal ini memperjelas apakah jenis tertentu yang Anda lihat sekarang memenuhi Node .

Pola ini berguna ketika sebagian besar tipe dalam penjumlahan didefinisikan dalam paket yang sama. Ketika tidak ada, jalan umum adalah kembali ke interface{} , seperti json.Token atau driver.Value . Kita bisa menggunakan pola sebelumnya dengan jenis pembungkus untuk masing-masing tetapi pada akhirnya dikatakan sebanyak interface{} jadi tidak ada gunanya. Jika kita mengharapkan nilai seperti itu datang dari luar paket, kita dapat bersikap sopan dan mendefinisikan pabrik:

//Sum is one of int64, float64, or bool.
type Sum interface{}
func New(v interface{}) (Sum, error) {
  switch v.(type) {
  case nil:
    return errors.New("pkg: Sum must be non-nil")
  case int64, float64, bool:
     return v
  }
  return fmt.Printf("pkg: %T is not a valid member of Sum")
}

Penggunaan jumlah yang umum adalah untuk tipe opsional, di mana Anda perlu membedakan antara "tidak ada nilai" dan "nilai yang mungkin nol". Ada dua cara untuk melakukan ini.

*T mari Anda menandakan tidak ada nilai sebagai pointer nil dan (mungkin) nilai nol sebagai hasil dari derefencing pointer non-nol.

Seperti perkiraan berbasis antarmuka sebelumnya, dan berbagai proposal untuk mengimplementasikan tipe jumlah sebagai antarmuka dengan batasan, ini memerlukan dereferensi pointer tambahan dan kemungkinan alokasi tumpukan.

Untuk opsional ini dapat dihindari dengan menggunakan teknik dari paket sql

type OptionalT struct {
  Valid bool
  Value T
}

Kelemahan utama dari ini adalah memungkinkan penyandian status tidak valid: Valid bisa salah dan Nilai bisa bukan nol. Dimungkinkan juga untuk mengambil Nilai ketika Valid salah (meskipun ini dapat berguna jika Anda menginginkan nol T jika tidak ditentukan). Secara santai menyetel Valid ke false tanpa memusatkan Nilai diikuti dengan menyetel Valid ke true (atau mengabaikannya) tanpa menetapkan Nilai menyebabkan nilai yang sebelumnya dibuang muncul kembali secara tidak sengaja. Ini dapat diatasi dengan menyediakan setter dan getter untuk melindungi invarian dari tipe tersebut.

Bentuk paling sederhana dari tipe penjumlahan adalah ketika Anda peduli dengan identitas, bukan nilai: enumerasi.

Cara tradisional untuk menangani ini di Go adalah const/iota:

type Enum int
const (
  A Enum = iota
  B
  C
)

Seperti tipe OptionalT ini tidak memiliki tipuan yang tidak perlu. Seperti jumlah antarmuka, itu tidak membatasi domain: hanya ada tiga nilai yang valid dan banyak nilai yang tidak valid, jadi kita perlu memvalidasi saat runtime. Jika ada tepat dua nilai, kita dapat menggunakan bool.

Ada juga masalah jumlah mendasar dari jenis ini. A+B == C . Kita dapat mengonversi konstanta integral tak bertipe ke tipe ini dengan sedikit terlalu mudah. Ada banyak tempat di mana itu diinginkan, tetapi kami mendapatkan ini apa pun yang terjadi. Dengan sedikit kerja ekstra, kita dapat membatasi ini hanya pada identitas:

type Enum struct { v int }
var (
  A = Enum{0}
  B = Enum{1}
  C = Enum{2}
)

Sekarang ini hanya label buram. Mereka dapat dibandingkan tetapi hanya itu. Sayangnya sekarang kami kehilangan keteguhan, tetapi kami bisa mendapatkannya kembali dengan sedikit lebih banyak pekerjaan:

func A() Enum { return Enum{0} }
func B() Enum { return Enum{1} }
func C() Enum { return Enum{2} }

Kami telah mendapatkan kembali ketidakmampuan pengguna eksternal untuk mengubah nama dengan mengorbankan beberapa boilerplate dan beberapa panggilan fungsi yang sangat inline-able.

Namun, ini dalam beberapa hal lebih bagus daripada jumlah antarmuka karena kami hampir sepenuhnya menutup jenisnya. Kode eksternal hanya dapat menggunakan A() , B() , atau C() . Mereka tidak dapat menukar label seperti pada contoh var dan mereka tidak dapat melakukan A() + B() dan kami bebas menentukan metode apa pun yang kami inginkan pada Enum . Kode dalam paket yang sama masih mungkin untuk membuat atau memodifikasi nilai secara keliru, tetapi jika kami berhati-hati untuk memastikan hal itu tidak terjadi, ini adalah jenis penjumlahan pertama yang tidak memerlukan kode validasi: jika ada, itu valid .

Terkadang Anda memiliki banyak label dan beberapa di antaranya memiliki tanggal tambahan dan label yang memiliki jenis data yang sama. Katakanlah Anda memiliki nilai yang memiliki tiga status tidak bernilai (A, B, C), dua dengan nilai string (D, E) dan satu dengan nilai string dan nilai int (F). Kita bisa menggunakan sejumlah kombinasi taktik di atas, tetapi cara paling sederhana adalah

type Value struct {
  Which int // could have consts for A, B, C, D, E, F
  String string
  Int int
}

Ini sangat mirip dengan tipe OptionalT atas, tetapi alih-alih bool, ia memiliki enumerasi dan ada beberapa bidang yang dapat disetel (atau tidak) tergantung pada nilai Which . Validasi harus berhati-hati bahwa ini diatur (atau tidak) dengan tepat.

Ada banyak cara untuk mengekspresikan "salah satu dari yang berikut" di Go. Beberapa membutuhkan lebih banyak perawatan daripada yang lain. Mereka sering membutuhkan validasi invarian "salah satu" pada saat runtime atau dereferensi asing. Kelemahan utama yang mereka semua bagikan adalah karena mereka disimulasikan dalam bahasa alih-alih menjadi bagian dari bahasa, invarian "salah satu" tidak muncul dalam refleksi atau go/tipe, sehingga sulit untuk metaprogram dengan mereka. Untuk menggunakannya dalam metaprogramming, Anda berdua harus dapat mengenali dan memvalidasi rasa jumlah yang benar dan diberi tahu bahwa itulah yang Anda cari karena semuanya sangat mirip dengan kode yang valid tanpa invarian "salah satu".

Jika tipe sum adalah bagian dari bahasa, mereka dapat direfleksikan dan dengan mudah ditarik keluar dari kode sumber, menghasilkan pustaka dan perkakas yang lebih baik. Kompilator dapat membuat sejumlah pengoptimalan jika ia mengetahui invarian "salah satu" itu. Pemrogram dapat fokus pada kode validasi penting daripada pemeliharaan sepele untuk memeriksa apakah suatu nilai memang berada dalam domain yang benar.

Konstruksi seperti ini mari kita katakan bahwa suatu tipe ada dalam jumlah, tetapi tidak membiarkan kita mengatakan apa yang tidak ada dalam jumlah itu. Selain kasus nil wajib, trik penyematan yang sama dapat digunakan oleh paket eksternal seperti
[…]
Di dalam paket kita harus berhati-hati untuk memvalidasi nilai yang dikompilasi tetapi ilegal.

Mengapa? Sebagai penulis paket, ini tampaknya benar-benar berada di ranah "masalah Anda" bagi saya. Jika Anda memberikan saya io.Reader , yang metode Read panik, saya tidak akan pulih dari itu dan membiarkannya panik. Demikian juga, jika Anda berusaha keras untuk menciptakan nilai yang tidak valid dari tipe yang saya nyatakan - siapa saya untuk berdebat dengan Anda? Yaitu saya menganggap "Saya menyematkan jumlah tertutup yang ditiru" sebagai masalah yang jarang (jika pernah) muncul secara tidak sengaja.

Karena itu, Anda dapat mencegah masalah itu, dengan mengubah antarmuka menjadi type Sum interface { sum() Sum } dan mengembalikan setiap nilai dengan sendirinya. Dengan begitu, Anda bisa menggunakan pengembalian sum() , yang akan berperilaku baik bahkan di bawah penyematan.

Salah satu keluhan yang lebih umum tentang pola ini adalah bahwa hal itu tidak membuat keanggotaan dalam jumlah jelas di godoc.

Ini dapat membantu Anda .

Kelemahan utama dari ini adalah memungkinkan penyandian status tidak valid: Valid bisa salah dan Nilai bisa bukan nol.

Ini bukan keadaan yang tidak valid bagi saya. Nilai nol tidak ajaib. Tidak ada perbedaan, IMO, antara sql.NullInt64{false,0} dan NullInt64{false,42} . Keduanya adalah representasi yang valid dan setara dari SQL NULL. Jika semua kode memeriksa Valid sebelum menggunakan Nilai, perbedaannya tidak dapat diamati pada suatu program.

Ini adalah kritik yang adil dan benar bahwa kompiler tidak memaksakan melakukan pemeriksaan ini (yang mungkin akan dilakukan, untuk jenis opsional/jumlah "nyata"), membuatnya lebih mudah untuk tidak melakukannya. Tetapi jika Anda melupakannya, saya tidak akan menganggapnya lebih baik untuk secara tidak sengaja menggunakan nilai nol daripada secara tidak sengaja menggunakan nilai bukan nol (dengan kemungkinan pengecualian tipe berbentuk pointer, karena mereka akan panik saat digunakan, jadi gagal keras - tetapi untuk itu, Anda harus tetap menggunakan tipe berbentuk pointer kosong dan menggunakan nil sebagai "tidak disetel").

Ada juga masalah jumlah mendasar dari jenis ini. A+B == C. Kita dapat mengonversi konstanta integral tak bertipe ke tipe ini dengan terlalu mudah.

Apakah ini masalah teoretis atau sudah muncul dalam praktik?

Pemrogram dapat fokus pada kode validasi penting daripada pemeliharaan sepele untuk memeriksa apakah suatu nilai memang berada dalam domain yang benar.

Hanya FTR, dalam kasus yang saya gunakan sum-types-as-sum-types (yaitu masalahnya tidak dapat dimodelkan dengan lebih elegan melalui antarmuka variasi emas) saya tidak pernah menulis kode validasi apa pun. Sama seperti saya tidak memeriksa nol-ness pointer yang diteruskan sebagai penerima atau argumen (kecuali jika didokumentasikan sebagai varian yang valid). Di tempat-tempat di mana kompiler memaksa saya untuk menghadapinya (yaitu masalah gaya "tidak ada pengembalian di akhir fungsi"), saya panik dalam kasus default.

Secara pribadi, saya menganggap Go sebagai bahasa pragmatis, yang tidak hanya menambahkan fitur keamanan untuk kepentingan mereka sendiri atau karena "semua orang tahu bahwa mereka lebih baik", tetapi berdasarkan kebutuhan yang ditunjukkan. Saya pikir menggunakannya dengan cara yang pragmatis baik-baik saja.

Sarana standar adalah antarmuka dengan metode yang tidak diekspor dan tidak melakukan apa-apa sebagai tag.

Ada perbedaan mendasar antara antarmuka dan tipe jumlah (saya tidak melihatnya disebutkan di posting Anda). Saat Anda memperkirakan jenis jumlah melalui antarmuka, benar-benar tidak ada cara untuk menangani nilainya. Sebagai konsumen, Anda tidak tahu apa yang sebenarnya ada di dalamnya, dan hanya bisa menebak. Ini tidak lebih baik daripada hanya menggunakan antarmuka kosong. Satu-satunya kegunaan adalah jika implementasi apa pun hanya dapat berasal dari paket yang sama yang mendefinisikan antarmuka, karena hanya dengan begitu Anda dapat mengontrol apa yang bisa Anda dapatkan.

Di sisi lain, memiliki sesuatu seperti:

func foo(val string|int|error) {
    switch v:= val.(type) {
    case string:
        ...
    }
}

Memberi konsumen kekuatan penuh dalam menggunakan nilai tipe penjumlahan. Nilainya konkrit, tidak terbuka untuk interpretasi.

@Merovius
"Jumlah terbuka" yang Anda sebutkan ini memiliki apa yang beberapa orang mungkin klasifikasikan sebagai kelemahan signifikan, karena mereka akan mengizinkan penyalahgunaannya untuk "fitur creep". Alasan ini telah diberikan mengapa argumen fungsi opsional telah ditolak sebagai fitur.

"Jumlah terbuka" yang Anda sebutkan ini memiliki apa yang beberapa orang mungkin klasifikasikan sebagai kelemahan signifikan, karena mereka akan mengizinkan penyalahgunaannya untuk "fitur creep". Alasan ini telah diberikan mengapa argumen fungsi opsional telah ditolak sebagai fitur.

Itu sepertinya argumen yang cukup lemah bagi saya - jika tidak ada yang lain, maka karena mereka ada, jadi Anda sudah mengizinkan apa pun yang mereka aktifkan. Memang, kami sudah memiliki argumen opsional , untuk semua maksud dan tujuan (bukan karena saya suka pola itu, tetapi jelas sudah dimungkinkan dalam bahasa).

Ada perbedaan mendasar antara antarmuka dan tipe jumlah (saya tidak melihatnya disebutkan di posting Anda). Saat Anda memperkirakan jenis jumlah melalui antarmuka, benar-benar tidak ada cara untuk menangani nilainya. Sebagai konsumen, Anda tidak tahu apa yang sebenarnya ada di dalamnya, dan hanya bisa menebak.

Saya sudah mencoba mem-parsing ini untuk kedua kalinya dan masih tidak bisa. Mengapa Anda tidak dapat menggunakannya? Mereka bisa berupa tipe reguler yang diekspor. Ya, mereka harus menjadi tipe yang dibuat dalam paket Anda (jelas), tetapi selain itu tampaknya tidak ada batasan dalam bagaimana Anda dapat menggunakannya, dibandingkan dengan jumlah tertutup yang sebenarnya.

Saya sudah mencoba mem-parsing ini untuk kedua kalinya dan masih tidak bisa. Mengapa Anda tidak dapat menggunakannya? Mereka bisa berupa tipe reguler yang diekspor. Ya, mereka harus menjadi tipe yang dibuat dalam paket Anda (jelas), tetapi selain itu tampaknya tidak ada batasan dalam bagaimana Anda dapat menggunakannya, dibandingkan dengan jumlah tertutup yang sebenarnya.

Apa yang terjadi jika metode dummy diekspor dan pihak ketiga mana pun dapat menerapkan "jenis jumlah"? Atau skenario yang cukup realistis di mana anggota tim tidak terbiasa dengan berbagai konsumen antarmuka, memutuskan untuk menambahkan implementasi lain dalam paket yang sama, dan contoh implementasi itu akhirnya diteruskan ke konsumen ini melalui berbagai cara kode? Dengan risiko mengulangi pernyataan saya yang "tidak dapat diuraikan": "Sebagai konsumen, Anda tidak tahu apa yang sebenarnya dimiliki [nilai penjumlahan], dan hanya bisa menebak.". Anda tahu, karena ini adalah antarmuka, dan itu tidak memberi tahu Anda siapa yang mengimplementasikannya.

@Merovius

Hanya FTR, dalam kasus yang saya gunakan sum-types-as-sum-types (yaitu masalahnya tidak dapat dimodelkan dengan lebih elegan melalui antarmuka variasi emas) saya tidak pernah menulis kode validasi apa pun. Sama seperti saya tidak memeriksa nol-ness pointer yang diteruskan sebagai penerima atau argumen (kecuali jika didokumentasikan sebagai varian yang valid). Di tempat-tempat di mana kompiler memaksa saya untuk menghadapinya (yaitu masalah gaya "tidak ada pengembalian di akhir fungsi"), saya panik dalam kasus default.

Saya tidak memperlakukan ini sebagai hal yang selalu atau tidak pernah .

Jika seseorang memberikan input yang buruk akan segera meledak, saya tidak peduli dengan kode validasi.

Tetapi jika seseorang memberikan input yang buruk pada akhirnya dapat menyebabkan kepanikan tetapi tidak akan muncul untuk sementara waktu, maka saya menulis kode validasi sehingga input yang buruk ditandai sesegera mungkin dan tidak ada yang harus mengetahui bahwa kesalahan itu diperkenalkan 150 membingkai di tumpukan panggilan (terutama karena mereka kemudian mungkin harus naik 150 bingkai lagi di tumpukan panggilan untuk mencari tahu di mana nilai buruk itu diperkenalkan).

Menghabiskan setengah menit sekarang untuk menghemat setengah jam debugging nanti adalah pragmatis. Terutama bagi saya karena saya membuat kesalahan bodoh sepanjang waktu dan semakin cepat saya disekolahkan semakin cepat saya dapat melanjutkan untuk membuat kesalahan bodoh berikutnya.

Jika saya memiliki fungsi yang mengambil pembaca dan segera mulai menggunakannya, saya tidak akan memeriksa nihil, tetapi jika fungsi adalah pabrik untuk struct yang tidak akan memanggil pembaca sampai metode tertentu dipanggil, saya akan periksa apakah nihil dan panik atau kembalikan kesalahan dengan sesuatu seperti "reader must not be nil" sehingga penyebab kesalahan sedekat mungkin dengan sumber kesalahan.

godoc -analisis

Saya sadar tetapi saya tidak menganggapnya berguna. Itu berjalan selama 40 menit di ruang kerja saya sebelum saya menekan ^C dan itu perlu di-refresh setiap kali sebuah paket diinstal atau dimodifikasi. Ada #20131 (bercabang dari utas ini!).

Karena itu, Anda dapat mencegah masalah itu, dengan mengubah antarmuka menjadi type Sum interface { sum() Sum } dan mengembalikan setiap nilai dengan sendirinya. Dengan begitu, Anda bisa menggunakan pengembalian sum() , yang akan berperilaku baik bahkan di bawah penyematan.

Saya belum menemukan itu berguna. Itu tidak memberikan manfaat lebih dari validasi eksplisit dan memberikan lebih sedikit validasi.

Apakah [fakta bahwa Anda dapat menambahkan anggota enumerasi const/iota] merupakan masalah teoretis atau telah muncul dalam praktik?

Yang khusus itu teoretis: Saya mencoba membuat daftar semua pro dan kontra yang dapat saya pikirkan, teoretis dan praktis. Poin saya yang lebih besar, bagaimanapun, adalah bahwa ada banyak cara untuk mencoba mengekspresikan "salah satu" invarian dalam bahasa yang cukup umum digunakan tetapi tidak sesederhana hanya menjadikannya semacam tipe dalam bahasa.

Apakah [fakta bahwa Anda dapat menetapkan integral yang tidak diketik ke enumerasi const/iota] merupakan masalah teoretis atau sudahkah muncul dalam praktik?

Yang itu telah muncul dalam praktik. Tidak butuh waktu lama untuk mencari tahu apa yang salah tetapi akan memakan waktu lebih sedikit jika kompiler mengatakan "di sana, baris itu — itu yang salah". Ada pembicaraan tentang cara lain untuk menangani kasus khusus itu, tetapi saya tidak melihat bagaimana mereka akan digunakan secara umum.

Ini bukan keadaan yang tidak valid bagi saya. Nilai nol tidak ajaib. Tidak ada perbedaan, IMO, antara sql.NullInt64{false,0} dan NullInt64{false,42} . Keduanya adalah representasi yang valid dan setara dari SQL NULL. Jika semua kode memeriksa Valid sebelum menggunakan Nilai, perbedaannya tidak dapat diamati pada suatu program.

Ini adalah kritik yang adil dan benar bahwa kompiler tidak memaksakan melakukan pemeriksaan ini (yang mungkin akan dilakukan, untuk jenis opsional/jumlah "nyata"), membuatnya lebih mudah untuk tidak melakukannya. Tetapi jika Anda melupakannya, saya tidak akan menganggapnya lebih baik untuk secara tidak sengaja menggunakan nilai nol daripada secara tidak sengaja menggunakan nilai bukan nol (dengan kemungkinan pengecualian tipe berbentuk pointer, karena mereka akan panik saat digunakan, jadi gagal keras - tetapi untuk itu, Anda harus tetap menggunakan tipe berbentuk pointer kosong dan menggunakan nil sebagai "tidak disetel").

Bahwa "Jika semua kode memeriksa Valid sebelum menggunakan Nilai" adalah tempat bug masuk dan apa yang dapat ditegakkan oleh kompiler. Saya memiliki bug seperti itu terjadi (walaupun dengan versi yang lebih besar dari pola itu, di mana ada lebih dari satu bidang nilai dan lebih dari dua status untuk diskriminator). Saya percaya/berharap saya menemukan semua ini selama pengembangan dan pengujian dan tidak ada yang lolos ke alam liar, tetapi alangkah baiknya jika kompiler bisa memberi tahu saya ketika saya membuat kesalahan itu dan saya bisa yakin bahwa satu-satunya cara salah satunya menyelinap melewati adalah jika ada bug di kompiler, cara yang sama akan memberitahu saya jika saya mencoba untuk menetapkan string ke variabel tipe int.

Dan, tentu saja, saya lebih suka *T untuk tipe opsional meskipun itu tidak memiliki biaya nol yang terkait dengannya, baik dalam ruang-waktu eksekusi dan dalam keterbacaan kode.

(Untuk contoh khusus itu, kode untuk mendapatkan nilai aktual atau nilai nol yang benar dengan proposal pemilihan adalah v, _ := nullable.[Value] yang ringkas dan aman.)

Itu sangat tidak saya inginkan. Jenis pilih harus jenis nilai,
seperti di Karat. Kata pertama mereka harus menjadi penunjuk ke GC Metadata, jika diperlukan.

Jika tidak, penggunaannya disertai dengan penalti kinerja yang mungkin
tidak dapat diterima. Bagi saya, pass 10:41 AM, "Josh Bleecher Snyder" <
[email protected]> menulis:

Dengan proposal pemilihan, Anda dapat memilih untuk memiliki ap atau *p yang memberi Anda lebih banyak
kontrol yang lebih besar atas pertukaran memori.

Alasan mengapa antarmuka mengalokasikan untuk menyimpan nilai skalar adalah agar Anda tidak melakukannya
harus membaca kata jenis untuk memutuskan apakah kata lainnya adalah a
penunjuk; lihat #8405 https://github.com/golang/go/issues/8405 untuk
diskusi. Pertimbangan implementasi yang sama kemungkinan akan berlaku untuk a
pilih tipe, yang mungkin berarti dalam praktiknya p akhirnya mengalokasikan dan menjadi
non-lokal pula.


Anda menerima ini karena Anda yang menulis utas.
Balas email ini secara langsung, lihat di GitHub
https://github.com/golang/go/issues/19412#issuecomment-323371837 , atau bisukan
benang
https://github.com/notifications/unsubscribe-auth/AGGWB-wQD75N44TGoU6LWQhjED_uhKGUks5sZaKbgaJpZM4MTmSr
.

@urandom

Apa yang terjadi jika metode dummy diekspor dan pihak ketiga mana pun dapat menerapkan "jenis jumlah"?

Ada perbedaan antara metode yang diekspor dan jenis yang diekspor. Kami sepertinya berbicara melewati satu sama lain. Bagi saya, ini tampaknya berfungsi dengan baik, tanpa perbedaan antara jumlah terbuka dan tertutup:

type X interface { x() X }
type IntX int
func (v IntX) x() X { return v }
type StringX string
func (v StringX) x() X { return v }
type StructX struct{
    Foo bool
    Bar int
}
func (v StructX) x() X { return v }

Tidak ada ekstensi di luar paket yang memungkinkan, namun konsumen paket dapat menggunakan, membuat, dan menyebarkan nilai-nilai seperti yang lainnya.

Anda dapat menyematkan X, atau salah satu tipe lokal yang memenuhinya, secara eksternal dan kemudian meneruskannya ke fungsi dalam paket Anda yang mengambil X.

Jika fungsi itu memanggil x, ia akan panik (jika X sendiri disematkan dan tidak disetel ke apa pun) atau mengembalikan nilai yang dapat dioperasikan oleh kode Anda—tetapi bukan itu yang diteruskan oleh penelepon, yang akan sedikit mengejutkan bagi penelepon (dan kode mereka sudah dicurigai jika mereka mencoba sesuatu seperti ini karena mereka tidak membaca dokumen).

Memanggil validator yang panik dengan pesan "jangan lakukan itu" sepertinya cara yang paling tidak mengejutkan untuk menanganinya dan membiarkan penelepon memperbaiki kodenya.

Jika fungsi itu memanggil x, ia akan panik […] atau mengembalikan nilai yang dapat dioperasikan oleh kode Anda—tetapi bukan itu yang diteruskan oleh penelepon, yang akan sedikit mengejutkan bagi penelepon

Seperti yang saya katakan di atas: Jika Anda terkejut, bahwa konstruksi yang disengaja dari nilai yang tidak valid tidak valid, Anda perlu memikirkan kembali harapan Anda. Tetapi bagaimanapun juga, bukan itu topik diskusi khusus ini dan akan sangat membantu untuk memisahkan argumen-argumen yang terpisah. Yang ini tentang @urandom yang mengatakan bahwa jumlah terbuka melalui antarmuka dengan metode tag tidak akan dapat diintrospeksi atau digunakan oleh paket lain. Saya menemukan bahwa klaim yang meragukan, akan sangat bagus jika bisa diklarifikasi.

Masalahnya adalah seseorang dapat membuat tipe yang tidak dalam jumlah yang dikompilasi dan dapat diteruskan ke paket Anda.

Tanpa menambahkan tipe penjumlahan yang tepat ke bahasa, ada tiga opsi untuk menanganinya

  1. mengabaikan situasi
  2. memvalidasi dan panik/mengembalikan kesalahan
  3. coba "lakukan apa yang Anda maksud" dengan secara implisit mengekstraksi nilai yang disematkan dan menggunakannya

3 tampak seperti campuran aneh dari 1 dan 2 bagi saya: Saya tidak melihat apa yang dibelinya.

Saya setuju bahwa "Jika Anda terkejut, bahwa konstruksi yang disengaja dari nilai yang tidak valid tidak valid, Anda perlu memikirkan kembali harapan Anda", tetapi, dengan 3, akan sangat sulit untuk menyadari bahwa ada sesuatu yang salah dan bahkan ketika Anda melakukannya akan sulit untuk mengetahui alasannya.

2 tampaknya paling baik karena keduanya melindungi kode agar tidak tergelincir ke keadaan tidak valid dan mengirimkan suar jika seseorang mengacaukannya, memberi tahu mereka mengapa mereka salah dan bagaimana cara memperbaikinya.

Apakah saya salah memahami maksud dari pola atau kita hanya mendekati ini dari filosofi yang berbeda?

@urandom Saya juga menghargai klarifikasi; Saya juga tidak 100% yakin dengan apa yang ingin Anda katakan.

Masalahnya adalah seseorang dapat membuat tipe yang tidak dalam jumlah yang dikompilasi dan dapat diteruskan ke paket Anda.

Anda selalu bisa melakukan itu; jika ragu, Anda selalu dapat menggunakan tidak aman, bahkan dengan tipe jumlah yang diperiksa oleh kompiler (dan saya tidak melihatnya sebagai cara yang berbeda secara kualitatif untuk membangun nilai yang tidak valid dari menyematkan sesuatu yang jelas dimaksudkan sebagai jumlah dan tidak menginisialisasi ke a nilai yang sah). Pertanyaannya adalah "seberapa sering hal ini akan menimbulkan masalah dalam praktik dan seberapa parah masalah itu". Menurut pendapat saya, dengan solusi dari atas jawabannya adalah "hampir tidak pernah dan sangat rendah" - Anda tampaknya tidak setuju, itu tidak masalah. Tetapi bagaimanapun juga, sepertinya tidak ada gunanya membahas ini - argumen dan pandangan di kedua sisi dari poin khusus ini harus cukup jelas dan saya mencoba untuk menghindari pengulangan yang terlalu berisik dan fokus pada yang benar-benar argumen baru. Saya mengemukakan konstruksi di atas untuk menunjukkan bahwa tidak ada perbedaan dalam kemampuan ekspor antara tipe jumlah kelas satu dan jumlah yang ditiru melalui antarmuka. Bukan untuk menunjukkan bahwa mereka benar-benar lebih baik dalam segala hal.

jika ragu, Anda selalu dapat menggunakan tidak aman, bahkan dengan tipe jumlah yang diperiksa oleh kompiler (dan saya tidak melihatnya sebagai cara yang berbeda secara kualitatif untuk membangun nilai yang tidak valid dari menyematkan sesuatu yang jelas dimaksudkan sebagai jumlah dan tidak menginisialisasi ke a nilai yang sah).

Saya pikir ini berbeda secara kualitatif: ketika orang menyalahgunakan penyematan dengan cara ini (setidaknya dengan proto.Message dan tipe konkret yang menerapkannya), mereka umumnya tidak memikirkan apakah itu aman dan invarian apa yang mungkin rusak . (Pengguna berasumsi bahwa antarmuka sepenuhnya menggambarkan perilaku yang diperlukan, tetapi ketika antarmuka digunakan sebagai tipe gabungan atau jumlah, mereka sering tidak melakukannya. Lihat juga https://github.com/golang/protobuf/issues/364.)

Sebaliknya, jika seseorang menggunakan paket unsafe untuk menetapkan variabel ke tipe yang biasanya tidak dapat dirujuk, mereka kurang lebih secara eksplisit mengklaim setidaknya memikirkan apa yang mungkin mereka rusak dan mengapa.

@Merovius Mungkin saya tidak jelas: fakta bahwa kompiler akan memberi tahu seseorang bahwa mereka menggunakan penyematan yang salah lebih merupakan manfaat sampingan yang bagus.

Keuntungan terbesar dari fitur keselamatan adalah bahwa fitur tersebut akan dihormati dengan mencerminkan dan diwakili dalam go/types. Itu memberi perkakas dan perpustakaan lebih banyak informasi untuk dikerjakan. Ada banyak cara untuk mensimulasikan tipe sum di Go tetapi semuanya identik dengan kode tipe non-sum, jadi perkakas dan pustaka perlu info band untuk mengetahui bahwa itu adalah tipe penjumlahan dan harus mampu mengenali pola tertentu digunakan tetapi bahkan pola-pola itu memungkinkan variasi yang signifikan.

Itu juga akan membuat tidak aman satu-satunya cara untuk membuat nilai yang tidak valid: sekarang Anda memiliki kode reguler, kode yang dihasilkan, dan refleksi—dua yang terakhir lebih cenderung menyebabkan masalah karena tidak seperti orang yang tidak dapat membaca dokumentasi.

Manfaat sisi lain dari keamanan berarti kompiler memiliki lebih banyak informasi dan dapat menghasilkan kode yang lebih cepat dan lebih baik.

Ada juga fakta bahwa selain dapat mengganti pseudo-sum dengan antarmuka, Anda dapat mengganti pseudo-sum "salah satu tipe reguler ini" seperti json.Token atau driver.Value . Itu sedikit dan jarang tetapi itu akan menjadi satu tempat yang lebih sedikit di mana interface{} diperlukan.

Itu juga akan membuat tidak aman satu-satunya cara untuk membuat nilai yang tidak valid

Saya rasa saya tidak mengerti definisi "nilai tidak valid" yang mengarah ke pernyataan ini.

@neild jika Anda punya

var v pick {
  None struct{}
  A struct { X int; Y *T}
  B int
}

itu akan diletakkan dalam memori seperti

struct {
  activeField int //which of None (0), A (1), or B (2) is the current field
  theInt int // If None always 0
  thePtr *T // If None or B, always nil
}

dan dengan tidak aman Anda dapat menetapkan thePtr bahkan jika activeField adalah 0 atau 2 atau menetapkan nilai theInt bahkan jika activeField adalah 0.

Dalam kedua kasus ini, ini akan membatalkan asumsi yang akan dibuat oleh kompiler dan memungkinkan jenis bug teoretis yang sama yang dapat kita miliki saat ini.

Tetapi seperti yang ditunjukkan @bcmils jika Anda menggunakan tidak aman, Anda sebaiknya tahu apa yang Anda lakukan karena itu adalah opsi nuklir.

Yang tidak saya mengerti adalah mengapa tidak aman adalah satu -

var t time.Timer

t adalah nilai yang tidak valid; t.C tidak disetel, panggilan t.Stop akan panik, dll. Tidak perlu tidak aman.

Beberapa bahasa memiliki sistem tipe yang berusaha keras untuk mencegah pembuatan nilai "tidak valid". Pergi bukan salah satunya. Saya tidak melihat bagaimana serikat pekerja menggerakkan jarum itu secara signifikan. (Tentu saja ada alasan lain untuk mendukung serikat pekerja.)

@neild ya maaf saya longgar dengan definisi saya.

Saya seharusnya mengatakan tidak valid sehubungan dengan invarian dari tipe jumlah .

Jenis individu dalam jumlah tentu saja bisa dalam keadaan tidak valid.

Namun, mempertahankan jumlah jenis invarian berarti mereka dapat diakses untuk mencerminkan dan pergi/ketik serta programmer sehingga memanipulasi mereka di perpustakaan dan alat menjaga keamanan itu dan memberikan lebih banyak informasi ke metaprogrammer

@jimmyfrasche , saya mengatakan bahwa tidak seperti tipe jumlah, yang memberi tahu Anda setiap jenis yang mungkin, antarmuka buram karena Anda tidak tahu, atau setidaknya Anda tidak dapat digunakan, apa daftar jenisnya yang mengimplementasikan antarmuka adalah. Ini membuat penulisan bagian switch dari kode sedikit menebak-nebak:

func F(sum SumInterface) {
    switch v := sum {
    case Screwdriver:
             ...
    default:
           panic ("Someone implementing a new type which gets passed to F and causes a runtime panic 3 weeks into production")
    }
}

Jadi, menurut saya, sebagian besar masalah yang dihadapi orang dengan emulasi tipe-jumlah berbasis antarmuka dapat diselesaikan dengan berdentang dan/atau konvensi. Misalnya jika sebuah antarmuka berisi metode yang tidak diekspor, akan sepele untuk mengetahui semua kemungkinan (ya, pengelakan yang disengaja) implementasi. Demikian pula, untuk mengatasi sebagian besar masalah dengan enum berbasis iota, konvensi sederhana "enum adalah type Foo int dengan deklarasi bentuk const ( FooA Foo = iota; FooB; FooC ) " akan memungkinkan untuk menulis alat yang luas dan tepat untuk mereka juga.

Ya, ini tidak setara dengan tipe jumlah aktual (antara lain, mereka tidak akan mendapatkan dukungan refleksi kelas satu, meskipun saya tidak begitu mengerti betapa pentingnya itu), tetapi itu berarti bahwa solusi yang ada muncul, dari POV saya, lebih baik daripada mereka sering dicat. Dan IMO, ada baiknya menjelajahi ruang desain itu sebelum benar-benar memasukkannya ke dalam Go 2 - setidaknya jika itu benar-benar penting bagi orang-orang.

(dan saya ingin menekankan kembali bahwa saya menyadari keuntungan dari tipe penjumlahan, jadi tidak perlu menyatakan kembali untuk keuntungan saya. Saya hanya tidak menimbangnya seberat orang lain, juga melihat kerugiannya dan dengan demikian sampai pada kesimpulan yang berbeda pada data yang sama)

@Merovius itu posisi yang bagus.

Dukungan refleksi akan memungkinkan perpustakaan serta alat off-line—linter, pembuat kode, dll.—untuk mengakses informasi dan melarangnya memodifikasinya secara tidak tepat yang tidak dapat dideteksi secara statis dengan presisi apa pun.

Terlepas dari itu, itu ide yang adil untuk dijelajahi, jadi mari kita jelajahi.

Untuk rekap keluarga pseudosum yang paling umum di Go adalah: (kira-kira dalam urutan kemunculannya)

  • const/iota enum.
  • Antarmuka dengan metode tag untuk jumlah lebih dari jenis yang didefinisikan dalam paket yang sama.
  • *T untuk T opsional
  • struct dengan enum yang nilainya menentukan bidang apa yang dapat disetel (ketika enum adalah bool dan hanya ada satu bidang lain, ini adalah jenis lain dari opsional T )
  • interface{} yang terbatas pada tas tangan dengan jenis yang terbatas.

Semua itu dapat digunakan untuk tipe sum dan non-sum. Dua yang pertama sangat jarang digunakan untuk hal lain sehingga mungkin masuk akal untuk berasumsi bahwa mereka mewakili tipe jumlah dan menerima positif palsu sesekali. Untuk jumlah antarmuka, itu dapat membatasinya ke metode yang tidak diekspor tanpa params atau pengembalian dan tanpa tubuh pada anggota mana pun. Untuk enum, masuk akal untuk hanya mengenalinya ketika mereka hanya Type = iota sehingga tidak tersandung ketika iota digunakan sebagai bagian dari ekspresi.

*T untuk T opsional akan sangat sulit dibedakan dari pointer biasa. Ini dapat diberikan konvensi type O = *T . Itu mungkin untuk dideteksi, meskipun agak sulit karena nama alias bukan bagian dari tipe. type O *T akan lebih mudah dideteksi tetapi lebih sulit untuk dikerjakan dalam kode. Di sisi lain segala sesuatu yang perlu dilakukan pada dasarnya dibangun ke dalam tipe sehingga ada sedikit yang bisa diperoleh dalam perkakas dari mengenali ini. Mari kita abaikan yang satu ini. (Generik kemungkinan akan mengizinkan sesuatu di sepanjang baris type Optional(T) *T yang akan menyederhanakan "menandai" ini).

Struct dengan enum akan sulit untuk dipikirkan dalam perkakas, bidang mana yang cocok dengan nilai enum yang mana? Kita dapat menyederhanakan ini dengan konvensi bahwa harus ada satu bidang per anggota di enum dan nilai enum dan nilai bidang harus sama, misalnya:

type Which int
const (
  A Which = iota
  B
  C
)
type Sum struct {
  Which
  A struct{} // has to be included to line up with the value of Which
  B struct { X int; Y float64 }
  C struct { X int; Y int } 
}

Itu tidak akan mendapatkan tipe opsional tetapi kami dapat membuat kasus khusus "2 bidang, pertama adalah bool" di pengenal.

Menggunakan interface{} untuk jumlah tas ambil tidak mungkin dideteksi tanpa komentar ajaib seperti //gosum: int, float64, string, Foo

Sebagai alternatif, mungkin ada paket khusus dengan definisi berikut:

package sum
type (
  Type struct{}
  Enum int
  OneOf interface{}
)

dan hanya mengenali enum jika berbentuk type MyEnum sum.Enum , hanya mengenali antarmuka dan struct hanya jika mereka menyematkan sum.Type , dan hanya mengenali interface{} ambil tas seperti type GrabBag sum.OneOf (tapi itu masih membutuhkan komentar yang dapat dikenali mesin untuk menjelaskan komentarnya). Itu akan memiliki pro dan kontra berikut:
kelebihan

  • eksplisit dalam kode: jika ditandai itu adalah 100% jenis jumlah, tidak ada positif palsu.
  • definisi tersebut dapat memiliki dokumentasi yang menjelaskan artinya dan dokumentasi paket dapat menautkan ke alat yang dapat digunakan dengan tipe ini
  • beberapa akan memiliki visibilitas dalam refleksi
    Kontra
  • Banyak negatif palsu dari kode lama dan stdlib (yang tidak akan menggunakannya).
  • Mereka harus digunakan agar berguna sehingga adopsi akan lambat dan kemungkinan tidak akan pernah mencapai 100% dan efektivitas alat yang mengenali paket khusus ini akan menjadi fungsi adopsi, sangat menarik meskipun percobaan tetapi kemungkinan tidak realistis.

Terlepas dari mana dari dua cara tersebut yang digunakan untuk mengidentifikasi tipe penjumlahan, mari kita asumsikan bahwa mereka dikenali dan beralih menggunakan informasi tersebut untuk melihat jenis perkakas apa yang dapat kita buat.

Secara kasar kita dapat mengelompokkan perkakas menjadi generatif (seperti stringer) dan introspektif (seperti golint).

Kode generatif paling sederhana akan menjadi alat untuk mengisi pernyataan switch dengan kasus yang hilang. Ini bisa digunakan oleh editor. Setelah tipe sum diidentifikasi sebagai tipe sum, ini sepele (sedikit melelahkan tetapi logika generasi sebenarnya akan sama dengan atau tanpa dukungan bahasa).

Dalam semua kasus, dimungkinkan untuk menghasilkan fungsi yang memvalidasi invarian "salah satu".

Untuk enum mungkin ada lebih banyak alat seperti stringer. Di https://github.com/golang/go/issues/19814#issuecomment -291002852 saya menyebutkan beberapa kemungkinan.

Alat generatif terbesar adalah kompiler yang dapat menghasilkan kode mesin yang lebih baik dengan info ini, tapi ah baiklah.

Saya tidak bisa memikirkan orang lain saat ini. Apakah ada sesuatu di daftar keinginan seseorang?

Untuk introspeksi, calon yang jelas adalah kesungguhan linting. Tanpa dukungan bahasa sebenarnya ada dua jenis linting yang diperlukan

  1. memastikan semua kemungkinan status ditangani
  2. memastikan tidak ada status tidak valid yang dibuat (yang akan membatalkan pekerjaan yang dilakukan oleh 1)

1 sepele, tetapi itu akan membutuhkan semua kemungkinan status dan kasus default karena 2 tidak dapat diverifikasi 100% (bahkan mengabaikan tidak aman) dan Anda tidak dapat mengharapkan semua kode menggunakan kode Anda menjalankan linter ini.

2 tidak dapat benar-benar mengikuti nilai melalui refleksi atau mengidentifikasi semua kode yang dapat menghasilkan status tidak valid untuk penjumlahan tetapi dapat menangkap banyak kesalahan sederhana, seperti jika Anda menyematkan tipe penjumlahan dan kemudian memanggil fungsi dengannya, bisa dikatakan "Anda menulis pkg.F(v) tetapi yang Anda maksud adalah pkg.F(v.EmbeddedField)" atau "Anda memberikan 2 ke pkg.F, gunakan pkg.B". Untuk struct itu tidak bisa berbuat banyak untuk menegakkan invarian bahwa satu bidang diatur pada satu waktu kecuali dalam kasus yang sangat jelas seperti "Anda mengaktifkan Yang dan dalam kasus X Anda mengatur bidang F ke nilai bukan nol ". Itu bisa memaksa Anda menggunakan fungsi validasi yang dihasilkan saat menerima nilai dari luar paket.

Hal besar lainnya akan muncul di godoc. godoc sudah mengelompokkan const/iota dan #20131 akan membantu dengan pseudosum antarmuka. Sebenarnya tidak ada hubungannya dengan versi struct yang tidak eksplisit dalam definisi selain untuk menentukan invarian.

serta alat offline—linter, pembuat kode, dll.

Tidak. Informasi statis ada, Anda tidak memerlukan sistem tipe (atau refleksi) untuk itu, konvensi berfungsi dengan baik. Jika antarmuka Anda berisi metode yang tidak diekspor, alat statis apa pun dapat memilih untuk memperlakukannya sebagai jumlah tertutup (karena memang demikian) dan melakukan analisis/kodegen apa pun yang Anda inginkan. Begitu juga dengan konvensi iota-enums.

reflect adalah untuk informasi tipe runtime - dan dalam arti tertentu, kompiler menghapus info yang diperlukan untuk membuat penjumlahan menurut konvensi berfungsi di sini (karena tidak memberi Anda akses ke daftar fungsi atau tipe yang dideklarasikan atau const yang dideklarasikan), yang itulah sebabnya saya setuju bahwa jumlah aktual memungkinkan ini.

(juga, FTR, tergantung pada kasus penggunaan, Anda masih dapat memiliki alat yang menggunakan informasi yang diketahui secara statis untuk menghasilkan informasi runtime yang diperlukan - misalnya dapat menghitung jenis yang memiliki metode tag yang diperlukan dan menghasilkan tabel pencarian untuk mereka Tapi saya tidak mengerti apa itu use-case, jadi sulit untuk mengevaluasi kepraktisannya).

Jadi, pertanyaan saya sengaja: Apa gunanya, agar info ini tersedia saat runtime?

Terlepas dari itu, itu ide yang adil untuk dijelajahi, jadi mari kita jelajahi.

Ketika saya mengatakan "jelajahi", saya tidak bermaksud "menyebutkannya dan berdebat tentang mereka dalam ruang hampa", maksud saya "menerapkan alat yang menggunakan konvensi ini dan melihat seberapa berguna/perlu/praktisnya".

Keuntungan dari laporan pengalaman adalah, mereka didasarkan pada pengalaman: Anda perlu melakukan sesuatu, Anda mencoba menggunakan mekanisme yang ada untuk itu, Anda menemukan bahwa itu tidak cukup. Ini memfokuskan diskusi pada kasus penggunaan yang sebenarnya (seperti dalam "kasus yang digunakan") dan memungkinkan untuk mengevaluasi solusi yang diusulkan terhadap mereka, terhadap alternatif yang dicoba dan untuk melihat, bagaimana solusi tidak akan memiliki perangkap yang sama.

Anda melewatkan bagian "mencoba menggunakan mekanisme yang ada untuk itu". Anda ingin memiliki pemeriksaan kelengkapan statis dari jumlah (masalah). Tulis alat yang menemukan antarmuka dengan metode yang tidak diekspor, apakah pemeriksaan lengkap untuk sakelar jenis apa pun yang digunakannya, gunakan alat itu untuk sementara waktu (gunakan mekanisme yang ada untuk itu). Menulis, di mana gagal.

Saya berpikir keras dan mulai mengerjakan pengenal statis berdasarkan pemikiran yang mungkin digunakan alat. Saya, saya kira, secara implisit mencari umpan balik dan lebih banyak ide (dan itu terbayar dengan menghasilkan info yang diperlukan untuk refleksi).

FWIW, jika saya di mana Anda, saya akan mengabaikan kasus kompleks dan fokus pada hal-hal yang berfungsi: a) metode yang tidak diekspor dalam antarmuka dan b) const-iota-enums sederhana, yang memiliki int sebagai tipe yang mendasari dan satu const- deklarasi format yang diharapkan. Menggunakan alat akan membutuhkan penggunaan salah satu dari dua solusi ini, tetapi IMO tidak apa-apa (untuk menggunakan alat kompiler, Anda juga perlu menggunakan jumlah secara eksplisit, jadi sepertinya oke).

Itu jelas merupakan tempat yang baik untuk memulai dan itu dapat dipanggil setelah menjalankannya pada sekumpulan besar paket dan melihat berapa banyak positif/negatif palsu yang ada

https://godoc.org/github.com/jimmyfrasche/closed

Masih banyak pekerjaan yang sedang berjalan. Saya tidak bisa berjanji bahwa saya tidak perlu menambahkan parameter tambahan ke konstruktor. Ini mungkin memiliki lebih banyak bug daripada tes. Tapi itu cukup bagus untuk dimainkan.

Ada contoh penggunaan di cmds/closed-exporer yang juga akan mencantumkan semua tipe tertutup yang terdeteksi dalam paket yang ditentukan oleh jalur impornya.

Saya mulai hanya mendeteksi semua antarmuka dengan metode yang tidak diekspor tetapi mereka cukup umum dan sementara beberapa dengan jelas menjumlahkan tipe yang lain jelas tidak. Jika saya hanya membatasinya pada konvensi metode tag kosong, saya kehilangan banyak tipe penjumlahan, jadi saya memutuskan untuk merekam keduanya secara terpisah dan menggeneralisasi paket sedikit di luar tipe penjumlahan ke tipe tertutup.

Dengan enum saya pergi ke arah lain dan hanya merekam setiap const non-bitset dari tipe yang ditentukan. Saya berencana untuk mengekspos bitset yang ditemukan juga.

Itu belum mendeteksi struct opsional atau antarmuka kosong yang ditentukan karena mereka akan memerlukan semacam komentar penanda, tetapi ia melakukan kasus khusus yang ada di stdlib.

Saya mulai hanya mendeteksi semua antarmuka dengan metode yang tidak diekspor tetapi mereka cukup umum dan sementara beberapa dengan jelas menjumlahkan tipe yang lain jelas tidak.

Saya akan merasa terbantu jika Anda dapat memberikan beberapa contoh yang tidak.

@Merovius maaf saya tidak menyimpan daftar. Saya menemukannya dengan menjalankan stdlib.sh (dalam cmds/closed-explorer). Jika saya menemukan contoh yang bagus lain kali saya bisa bermain dengan ini, saya akan mempostingnya.

Yang saya tidak pertimbangkan sebagai tipe sum adalah semua antarmuka yang tidak diekspor yang digunakan untuk menyambungkan salah satu dari beberapa implementasi: tidak ada yang peduli apa yang ada di antarmuka, hanya saja ada sesuatu yang memuaskannya. Mereka sangat banyak digunakan sebagai antarmuka bukan penjumlahan, tetapi kebetulan ditutup karena tidak diekspor. Mungkin itu perbedaan tanpa perbedaan, tapi saya selalu bisa berubah pikiran setelah penyelidikan lebih lanjut.

@jimmyfrasche Saya berpendapat itu harus diperlakukan dengan benar sebagai jumlah tertutup. Saya berpendapat bahwa jika mereka tidak peduli dengan tipe dinamis (yaitu hanya memanggil metode di antarmuka), maka linter statis tidak akan mengeluh, karena "semua sakelar sudah lengkap" - jadi tidak ada kerugian untuk memperlakukannya sebagai penjumlahan tertutup. Jika, OTOH, mereka kadang-kadang melakukan pergantian tipe dan meninggalkan sebuah kasus, mengeluh akan benar - itu akan menjadi hal yang seharusnya ditangkap oleh linter.

Saya ingin memberikan kata yang bagus untuk menjelajahi bagaimana tipe serikat pekerja dapat mengurangi penggunaan memori. Saya sedang menulis juru bahasa di Go dan memiliki tipe Nilai yang harus diimplementasikan sebagai antarmuka karena Nilai dapat menjadi penunjuk ke tipe yang berbeda. Ini mungkin berarti []Nilai membutuhkan memori dua kali lebih banyak dibandingkan dengan mengemas pointer dengan tag bit kecil seperti yang dapat Anda lakukan di C. Sepertinya banyak?

Spesifikasi bahasa tidak perlu menyebutkan ini, tetapi sepertinya memotong penggunaan memori array menjadi dua untuk beberapa jenis serikat kecil bisa menjadi argumen yang cukup menarik untuk serikat pekerja? Ini memungkinkan Anda melakukan sesuatu yang sejauh yang saya tahu tidak mungkin dilakukan di Go hari ini. Sebaliknya, mengimplementasikan serikat pekerja di atas antarmuka dapat membantu ketepatan dan pemahaman program, tetapi tidak melakukan sesuatu yang baru di tingkat mesin.

Saya belum melakukan pengujian kinerja; hanya menunjukkan arah untuk penelitian.

Anda dapat menerapkan Nilai sebagai unsafe.Pointer sebagai gantinya.

Pada 6 Februari 2018 15:54, "Brian Slesinsky" [email protected] menulis:

Saya ingin memberikan kata yang bagus untuk mengeksplorasi bagaimana tipe serikat pekerja dapat berkurang
penggunaan memori. Saya sedang menulis juru bahasa di Go dan memiliki tipe Nilai yang
harus diimplementasikan sebagai antarmuka karena Nilai dapat menjadi petunjuk
untuk jenis yang berbeda. Ini mungkin berarti []Nilai membutuhkan dua kali lebih banyak
memori dibandingkan dengan mengemas pointer dengan sedikit tag seperti yang bisa Anda lakukan
di C. Sepertinya banyak?

Spesifikasi bahasa tidak perlu menyebutkan ini, tetapi sepertinya memotong memori
penggunaan array menjadi dua untuk beberapa jenis serikat kecil bisa menjadi cantik
argumen yang meyakinkan untuk serikat pekerja? Ini memungkinkan Anda melakukan sesuatu yang sejauh saya
tahu tidak mungkin dilakukan di Go hari ini. Sebaliknya, menerapkan serikat pekerja pada
atas antarmuka dapat membantu dengan kebenaran program dan
dimengerti, tetapi tidak melakukan sesuatu yang baru di tingkat mesin.

Saya belum melakukan pengujian kinerja; hanya menunjukkan arah untuk
riset.


Anda menerima ini karena Anda disebutkan.
Balas email ini secara langsung, lihat di GitHub
https://github.com/golang/go/issues/19412#issuecomment-363561070 , atau bisukan
benang
https://github.com/notifications/unsubscribe-auth/AGGWBz-L3t0YosVIJmYNyf2iQ-YgIXLGks5tSLv9gaJpZM4MTmSr
.

@skybrian Tampaknya cukup lancang mengenai implementasi tipe jumlah. Itu tidak hanya membutuhkan tipe-jumlah, tetapi juga bahwa kompiler mengenali kasus khusus hanya pointer dalam jumlah dan mengoptimalkannya sebagai pointer yang dikemas - dan itu mengharuskan GC untuk menyadari berapa banyak tag-bit yang diperlukan dalam pointer , untuk menutupi mereka. Seperti, saya tidak benar-benar melihat hal ini terjadi, TBH.

Itu memberi Anda: Jumlah jenis mungkin akan ditandai serikat pekerja dan mungkin akan memakan banyak ruang dalam irisan seperti sekarang. Kecuali jika irisannya homogen, tetapi Anda juga dapat menggunakan jenis irisan yang lebih spesifik sekarang.

Jadi ya. Dalam kasus yang sangat khusus, Anda mungkin dapat menghemat sedikit memori, jika Anda secara khusus mengoptimalkannya, tetapi tampaknya Anda juga dapat mengoptimalkannya secara manual, jika Anda benar-benar membutuhkannya.

@DemiMarie tidak aman.Pointer tidak berfungsi di App Engine, dan bagaimanapun, itu tidak akan membiarkan Anda mengemas bit tanpa mengacaukan pengumpul sampah. Bahkan jika itu mungkin, itu tidak akan portabel.

@Merovius ya, memang perlu mengubah runtime dan pengumpul sampah untuk memahami tata letak memori yang dikemas. Itulah intinya; pointer dikelola oleh runtime Go, jadi jika Anda ingin melakukan yang lebih baik daripada antarmuka dengan cara yang aman, Anda tidak dapat melakukannya di perpustakaan, atau di kompiler.

Tetapi saya akan dengan mudah mengakui bahwa menulis penerjemah cepat adalah kasus penggunaan yang tidak umum. Mungkin ada orang lain? Sepertinya cara yang baik untuk memotivasi fitur bahasa adalah dengan menemukan hal-hal yang tidak mudah dilakukan di Go hari ini.

Itu benar.

Menurut saya, Go bukanlah bahasa terbaik untuk menulis penerjemah,
karena sangat dinamisnya perangkat lunak tersebut. Jika Anda membutuhkan kinerja tinggi,
loop panas Anda harus ditulis dalam perakitan. Apakah ada alasan Anda?
perlu menulis juru bahasa yang berfungsi di App Engine?

Pada 6 Februari 2018 18:15, "Brian Slesinsky" [email protected] menulis:

@DemiMarie https://github.com/demimarie unsafe.Pointer tidak berfungsi di Aplikasi
Mesin, dan bagaimanapun, itu tidak akan membiarkan Anda mengepak barang tanpa
mengacaukan pemulung. Bahkan jika itu mungkin, itu tidak akan
portabel.

@metrovius ya, memang perlu mengubah runtime dan pengumpul sampah
untuk memahami tata letak memori yang dikemas. Itulah intinya; petunjuk adalah
dikelola oleh runtime Go, jadi jika Anda ingin melakukan yang lebih baik daripada antarmuka di a
cara yang aman, Anda tidak dapat melakukannya di perpustakaan, atau di kompiler.

Tetapi saya akan dengan mudah mengakui bahwa menulis penerjemah cepat adalah penggunaan yang tidak biasa
kasus. Mungkin ada orang lain? Sepertinya cara yang baik untuk memotivasi
fitur bahasa adalah untuk menemukan hal-hal yang tidak dapat dilakukan dengan mudah di Go hari ini.


Anda menerima ini karena Anda disebutkan.
Balas email ini secara langsung, lihat di GitHub
https://github.com/golang/go/issues/19412#issuecomment-363598572 , atau bisu
benang
https://github.com/notifications/unsubscribe-auth/AGGWB65jRKg_qVPWTiq8LbGk3YM1RUasks5tSN0tgaJpZM4MTmSr
.

Saya menemukan proposal @rogpeppe cukup menarik. Saya juga bertanya-tanya apakah ada potensi untuk membuka manfaat tambahan yang sejalan dengan yang sudah diidentifikasi oleh @griesemer.

Proposal tersebut mengatakan: "Set metode dari tipe jumlah memegang persimpangan set metode
dari semua jenis komponennya, tidak termasuk metode apa pun yang memiliki kesamaan
nama tapi tanda tangan yang berbeda.".

Tetapi tipe lebih dari sekedar kumpulan metode. Bagaimana jika tipe penjumlahan mendukung perpotongan operasi yang didukung oleh tipe komponennya?

Misalnya, pertimbangkan:

var x int|float64

Idenya adalah bahwa yang berikut ini akan berhasil.

x += 5

Itu akan sama dengan menulis sakelar tipe lengkap:

switch i := x.(type) {
case int:
    x = i + 5
case float64:
    x = i + 5
}

Varian lain melibatkan sakelar tipe di mana tipe komponen itu sendiri adalah tipe jumlah.

type Num int | float64
type StringOrNum string | Num 
var x StringOrNum

switch i := x.(type) {
case string:
    // Do string stuff.
case Num:
    // Would be nice if we could use i as a Num here.
}

Juga, saya pikir ada potensi sinergi yang sangat bagus antara tipe jumlah dan sistem generik yang menggunakan batasan tipe.

var x int|float64

Bagaimana dengan var x, y int | float64 ? Apa aturan di sini, saat menambahkan ini? Konversi lossy mana yang dibuat (dan mengapa)? Apa yang akan menjadi jenis hasil?

Go tidak melakukan konversi otomatis dalam ekspresi (seperti yang dilakukan C) dengan sengaja - pertanyaan ini tidak mudah dijawab dan menyebabkan bug.

Dan untuk lebih menyenangkan:

var x, y, z int|string|rune
x = 42
y = 'a'
z = "b"
fmt.Println(x + y + z)
fmt.Println(x + z + y)
fmt.Println(y + x + z)
fmt.Println(y + z + x)
fmt.Println(z + x + y)
fmt.Println(z + y + x)

Semua int , string dan rune memiliki + operator; apa cetakan di atas, mengapa dan yang terpenting, bagaimana hasilnya tidak membingungkan?

Bagaimana dengan var x, y int | float64 ? Apa aturan di sini, saat menambahkan ini? Konversi lossy mana yang dibuat (dan mengapa)? Apa yang akan menjadi jenis hasil?

@Merovius tidak ada konversi lossy yang dibuat secara implisit, meskipun saya dapat melihat bagaimana kata-kata saya dapat memberikan kesan itu maaf. Di sini, x + y tidak akan dikompilasi karena menyiratkan kemungkinan konversi implisit. Tetapi salah satu dari berikut ini akan dikompilasi:

z = int(x) + int(y)
z = float64(x) + float64(y)

Demikian pula contoh xyz Anda tidak akan dikompilasi karena memerlukan kemungkinan konversi implisit.

Saya pikir "mendukung persimpangan operasi yang didukung" terdengar bagus tetapi tidak cukup menyampaikan apa yang saya maksudkan. Menambahkan sesuatu seperti "kompilasi untuk semua jenis komponen" membantu menjelaskan cara kerjanya menurut saya.

Contoh lain adalah jika semua tipe komponen adalah irisan dan peta. Akan menyenangkan untuk dapat memanggil len pada tipe sum tanpa memerlukan sakelar tipe.

Semua int, string dan rune memiliki operator +; apa cetakan di atas, mengapa dan yang terpenting, bagaimana hasilnya tidak membingungkan?

Hanya ingin menambahkan bahwa "Bagaimana jika tipe jumlah mendukung persimpangan operasi yang didukung oleh tipe komponennya?" terinspirasi oleh deskripsi Go Spec tentang suatu tipe sebagai "Tipe menentukan satu set nilai bersama dengan operasi dan metode khusus untuk nilai-nilai itu.".

Poin yang saya coba sampaikan adalah bahwa suatu tipe lebih dari sekadar nilai dan metode, dan dengan demikian tipe jumlah dapat mencoba menangkap kesamaan dari hal-hal lain itu dari tipe komponennya. "Hal-hal lain" ini lebih bernuansa daripada hanya satu set operator.

Contoh lain adalah perbandingan dengan nil:

var x []int | []string
fmt.Println(x == nil)  // Prints true
x = []string(nil)
fmt.Println(x == nil)  // Still prints true

Kedua tipe komponen tersebut Setidaknya satu tipe sebanding dengan nil, jadi kami mengizinkan tipe sum dibandingkan dengan nil tanpa sakelar tipe. Tentu saja ini agak bertentangan dengan bagaimana antarmuka saat ini berperilaku, tetapi itu mungkin bukan hal yang buruk per https://github.com/golang/go/issues/22729

Sunting: pengujian kesetaraan adalah contoh buruk di sini karena saya pikir itu harus lebih permisif, dan hanya memerlukan kecocokan potensial dari satu atau lebih jenis komponen. Cermin tugas dalam hal itu.

Masalahnya adalah, bahwa hasilnya akan a) memiliki masalah yang sama dengan konversi otomatis atau b) akan sangat (dan membingungkan IMO) dalam lingkup terbatas - yaitu, semua operator hanya akan bekerja dengan literal yang tidak diketik, paling banter.

Saya juga memiliki masalah lain, yaitu mengizinkan itu akan semakin membatasi ketahanannya terhadap evolusi tipe konstituennya - sekarang satu-satunya tipe yang dapat Anda tambahkan sambil mempertahankan kompatibilitas mundur adalah yang memungkinkan semua operasi tipe konstituennya.

Semua ini tampak sangat berantakan bagi saya, untuk manfaat nyata yang sangat kecil (jika ada).

sekarang satu-satunya tipe yang dapat Anda tambahkan sambil mempertahankan kompatibilitas mundur adalah tipe yang memungkinkan semua operasi tipe konstituennya.

Oh dan secara eksplisit tentang yang ini juga: Ini menyiratkan bahwa Anda tidak pernah dapat memutuskan bahwa Anda ingin memperluas parameter atau mengembalikan tipe atau variabel atau… dari tipe tunggal ke jumlah. Karena menambahkan tipe baru akan membuat beberapa operasi (seperti tugas) gagal untuk dikompilasi.l

@Merovius perhatikan bahwa varian masalah kompatibilitas sudah ada dengan proposal asli karena "set metode dari tipe jumlah memegang persimpangan set metode
dari semua jenis komponennya". Jadi, jika Anda menambahkan jenis komponen baru yang tidak mengimplementasikan kumpulan metode tersebut, maka itu akan menjadi perubahan yang tidak kompatibel ke belakang.

Oh dan secara eksplisit tentang yang ini juga: Ini menyiratkan bahwa Anda tidak pernah dapat memutuskan bahwa Anda ingin memperluas parameter atau mengembalikan tipe atau variabel atau… dari tipe tunggal ke jumlah. Karena menambahkan tipe baru akan membuat beberapa operasi (seperti tugas) gagal untuk dikompilasi.l

Perilaku penugasan akan tetap seperti yang dijelaskan oleh @rogpeppe tetapi secara keseluruhan saya tidak yakin saya mengerti hal ini.

Jika tidak ada yang lain, saya pikir proposal rogpeppe asli perlu diklarifikasi mengenai perilaku tipe jumlah di luar sakelar tipe. Tugas dan set metode tercakup, tetapi hanya itu. Bagaimana dengan kesetaraan? Saya pikir kita bisa melakukan lebih baik daripada yang dilakukan antarmuka{}:

var x int | float64
fmt.Println(x == "hello")  // compilation error?
x = 0.0
fmt.Println(x == 0) // true or false?  I vote true :-)

Jadi jika Anda menambahkan tipe komponen baru yang tidak mengimplementasikan kumpulan metode itu, maka itu akan menjadi perubahan yang tidak kompatibel.

Anda selalu dapat menambahkan metode, tetapi Anda tidak dapat membebani operator untuk mengerjakan tipe baru. Itulah perbedaannya - dalam proposal mereka, Anda hanya dapat memanggil metode umum pada nilai jumlah (atau menetapkannya), kecuali jika Anda membukanya dengan tipe-pernyataan/-switch. Jadi, selama jenis yang Anda tambahkan memiliki metode yang diperlukan, itu tidak akan menjadi perubahan yang merusak. Dalam proposal Anda, itu masih akan menjadi perubahan besar, karena pengguna mungkin menggunakan operator yang tidak dapat Anda bebankan secara berlebihan.

(Anda mungkin ingin menunjukkan bahwa menambahkan jenis penjumlahan tersebut masih akan menjadi perubahan melanggar, karena jenis-switch tidak akan memiliki tipe baru di dalamnya Yang persis mengapa saya tidak mendukung proposal awal baik -. Aku tidak ingin jumlah tertutup karena alasan itu)

Perilaku penugasan akan tetap seperti yang dijelaskan oleh @rogpeppe

Proposal mereka hanya berbicara tentang penugasan ke nilai-jumlah, saya berbicara tentang penugasan dari nilai-jumlah (ke salah satu bagian penyusunnya). Saya setuju bahwa proposal mereka juga tidak mengizinkan ini, tetapi perbedaannya adalah, proposal mereka bukan tentang menambahkan kemungkinan ini. yaitu argumen saya persis, bahwa semantik yang Anda sarankan tidak terlalu bermanfaat, karena dalam praktiknya, penggunaan yang mereka dapatkan sangat terbatas.

fmt.Println(x == "hello") // compilation error?

Ini mungkin akan ditambahkan ke proposal mereka juga. Kami sudah memiliki kasus khusus yang setara untuk antarmuka , yaitu

Nilai x dari tipe non-interface X dan nilai t dari tipe interface T sebanding ketika nilai tipe X sebanding dan X mengimplementasikan T. Mereka sama jika tipe dinamis t identik dengan X dan nilai dinamis t sama dengan x .

fmt.Println(x == 0) // true or false? I vote true :-)

Agaknya palsu. Mengingat, bahwa serupa

var x int|float64 = 0.0
y := 0
fmt.Println(x == y)

harus menjadi kesalahan kompilasi (seperti yang kami simpulkan di atas), pertanyaan ini hanya benar-benar masuk akal ketika membandingkan dengan konstanta numerik yang tidak diketik. Pada saat itu agak tergantung bagaimana ini ditambahkan ke spesifikasi. Anda dapat berargumen, bahwa ini mirip dengan menetapkan konstanta ke tipe antarmuka dan karenanya harus memiliki tipe default (dan kemudian perbandingannya akan salah). IMO mana yang lebih dari baik, kami sudah menerima situasi itu hari ini tanpa banyak kebingungan. Namun, Anda juga dapat menambahkan kasus ke spesifikasi untuk konstanta yang tidak diketik yang akan mencakup kasus menugaskan/membandingkannya dengan jumlah dan menyelesaikan pertanyaan dengan cara itu.

Namun, menjawab pertanyaan ini dengan cara apa pun, tidak mengharuskan semua ekspresi menggunakan tipe jumlah yang mungkin masuk akal untuk bagian-bagian penyusunnya.

Tetapi untuk menegaskan kembali: Saya tidak berdebat mendukung proposal yang berbeda untuk jumlah. Saya berdebat dengan yang satu ini.

fmt.Println(x == "hello") // compilation error?

Ini mungkin akan ditambahkan ke proposal mereka juga.

Koreksi: Spesifikasi sudah mencakup kesalahan kompilasi ini, mengingat itu berisi pernyataan

Dalam perbandingan apa pun, operan pertama harus dapat ditetapkan ke jenis operan kedua, atau sebaliknya.

@Merovius Anda membuat beberapa poin bagus tentang varian proposal saya. Saya akan menahan diri untuk tidak memperdebatkannya lebih jauh, tetapi saya ingin menelusuri perbandingan ke pertanyaan 0 sedikit lebih jauh karena ini berlaku sama untuk proposal asli.

fmt.Println(x == 0) // true or false? I vote true :-)

Agaknya palsu. Mengingat, bahwa serupa

var x int|float64 = 0.0
y := 0
fmt.Println(x == y)
harus menjadi kesalahan kompilasi (seperti yang kami simpulkan di atas),

Saya tidak menemukan contoh ini sangat menarik karena jika Anda mengubah baris pertama menjadi var x float64 = 0.0 maka Anda dapat menggunakan alasan yang sama untuk menyatakan bahwa membandingkan float64 dengan 0 seharusnya salah. (Poin kecil: (a) Saya berasumsi maksud Anda float64(0) pada baris pertama, karena 0.0 dapat ditetapkan ke int. (b) x==y seharusnya tidak menjadi kesalahan kompilasi dalam contoh Anda. Seharusnya mencetak false.)

Saya pikir ide Anda bahwa "bahwa ini mirip dengan menetapkan konstanta ke tipe antarmuka dan karenanya harus memiliki tipe default" lebih menarik (dengan asumsi Anda maksud tipe sum), jadi contohnya adalah:

var x,y int|float64 = float64(0), 0
fmt.Println(x == y) // salah

Saya masih berpendapat bahwa x == 0 seharusnya benar. Model mental saya adalah bahwa suatu tipe diberikan ke 0 selambat mungkin. Saya menyadari bahwa ini bertentangan dengan perilaku antarmuka saat ini, itulah sebabnya saya mengangkatnya. Saya setuju bahwa ini tidak mengarah pada "banyak ketidakjelasan", tetapi masalah serupa dalam membandingkan antarmuka dengan nil telah menghasilkan cukup banyak kebingungan. Saya percaya kita akan melihat jumlah kebingungan yang sama untuk dibandingkan dengan 0 jika tipe jumlah muncul dan semantik kesetaraan lama disimpan.

Saya tidak menemukan contoh ini sangat menarik karena jika Anda mengubah baris pertama menjadi var x float64 = 0.0 maka Anda dapat menggunakan alasan yang sama untuk menyatakan bahwa membandingkan float64 dengan 0 seharusnya salah.

Saya tidak mengatakan itu harus , saya mengatakan bahwa mungkin itu akan terjadi , mengingat apa yang saya anggap sebagai tradeoff yang paling mungkin antara kesederhanaan/kegunaan untuk bagaimana proposal mereka akan diimplementasikan. Saya tidak mencoba membuat penilaian nilai. Bahkan, jika hanya dengan aturan sederhana kita bisa mencetaknya dengan benar, saya mungkin cenderung lebih menyukainya. Aku hanya tidak optimis.

Perhatikan, bahwa membandingkan float64(0) dengan int(0) (yaitu contoh dengan jumlah yang diganti dengan var x float64 = 0.0 ) bukanlah false , meskipun demikian, ini adalah waktu kompilasi kesalahan (sebagaimana mestinya). Ini adalah poin saya ; proposal Anda hanya benar-benar berguna bila dikombinasikan dengan konstanta yang tidak diketik, karena untuk hal lain itu tidak akan dikompilasi.

(a) Saya berasumsi maksud Anda float64(0) pada baris pertama, karena 0.0 dapat ditetapkan ke int.

Tentu (saya berasumsi semantik lebih dekat ke "tipe default" saat ini untuk ekspresi konstan, tetapi saya setuju bahwa kata-kata saat ini tidak menyiratkan itu).

(b) x==y seharusnya tidak menjadi kesalahan kompilasi dalam contoh Anda. Itu harus mencetak false.)

Tidak, itu seharusnya kesalahan waktu kompilasi. Anda telah mengatakan, bahwa operasi e1 == y , dengan e1 menjadi ekspresi tipe-jumlah harus diizinkan jika dan hanya jika ekspresi akan dikompilasi dengan pilihan tipe konstituen apa pun. Mengingat bahwa dalam contoh saya, x memiliki tipe int|float64 dan y memiliki tipe int dan mengingat float64 dan int tidak sebanding, kondisi ini jelas dilanggar.

Untuk membuat kompilasi ini, Anda harus menghapus syarat bahwa ekspresi yang diketik konstituen harus dikompilasi juga; di titik mana kita berada dalam situasi harus menyiapkan aturan bagaimana tipe dipromosikan atau dikonversi ketika digunakan dalam ekspresi ini (juga dikenal sebagai "kekacauan C").

Konsensus masa lalu adalah bahwa tipe jumlah tidak banyak menambah tipe antarmuka.

Mereka memang tidak untuk sebagian besar kasus penggunaan Go: layanan dan utilitas jaringan sepele. Tapi begitu sistem tumbuh lebih besar, ada kemungkinan besar mereka berguna.
Saat ini saya sedang menulis layanan yang sangat terdistribusi dengan jaminan konsistensi data yang diterapkan melalui banyak logika dan saya melaju ke situasi di mana mereka akan berguna. NPD ini menjadi terlalu mengganggu saat layanan tumbuh besar dan kami tidak melihat cara yang masuk akal untuk membaginya.
Maksud saya, jaminan sistem tipe Go agak terlalu lemah untuk sesuatu yang lebih kompleks daripada layanan jaringan primitif biasa.

Tapi, cerita dengan Rust menunjukkan itu adalah ide yang buruk untuk menggunakan tipe sum untuk NPD dan penanganan kesalahan seperti yang mereka lakukan di Haskell: ada alur kerja imperatif alami yang khas dan pendekatan Haskellish tidak cocok dengan itu.

Contoh

pertimbangkan iotuils.WriteFile -fungsi seperti dalam kodesemu. Aliran imperatif akan terlihat seperti ini

file = open(name, os.write)
if file is error
    return error("cannot open " + name + " writing: " + file.error)
if file.write(data) is error:
    return error("cannot write into " + name + " : " + file.error)
return ok

dan bagaimana tampilannya di Rust

match open(name, os.write)
    file
        match file.write(data, os.write)
            err
                return error("cannot open " + name + " writing: " + err)
            ok
                return ok
    err
        return error("cannot write into " + name + " : " + err)

itu aman tapi jelek.

Dan usulan saya:

type result[T, Err] oneof {
    default T
    Error Err
}

dan bagaimana tampilan programnya ( result[void, string] = !void )

file := os.Open(name, ...)
if !file {
    return result.Error("cannot open " + name + " writing: " + file.Error)
}
if res := file.Write(data); !res {
    return result.Error("cannot write into " + name + " : " + res.Error)
}
return ok

Disini default branch adalah anonymous dan error branch dapat diakses dengan .Error (setelah diketahui hasilnya adalah Error). Setelah diketahui file berhasil dibuka, pengguna dapat mengaksesnya melalui variabel itu sendiri. Pertama jika kita memastikan file berhasil dibuka atau keluar sebaliknya (dan dengan demikian pernyataan lebih lanjut mengetahui file tersebut bukan kesalahan).

Seperti yang Anda lihat, pendekatan ini mempertahankan aliran imperatif dan memberikan keamanan tipe. Penanganan NPD dapat dilakukan dengan cara yang serupa:

type Reference[T] oneof {
    default T
    nil
}
// Reference[T] = *T

penanganannya mirip dengan hasil

@sirkon , contoh Rust Anda tidak meyakinkan saya bahwa ada yang salah dengan tipe penjumlahan langsung seperti di Rust. Sebaliknya, ini menyarankan bahwa pencocokan pola pada tipe jumlah dapat dibuat lebih mirip Go menggunakan pernyataan if . Sesuatu seperti:

ferr := os.Open(name, ...)
if err(e) := ferr {           // conditional match and unpack, initializing e
  return fmt.Errorf("cannot open %v: %v", name, e)
}
ok(f) := ferr                  // unconditional match and unpack, initializing f
werr := f.Write(data)
...

(Dalam semangat tipe jumlah, itu akan menjadi kesalahan kompilasi jika kompiler tidak dapat membuktikan bahwa kecocokan tanpa syarat selalu berhasil karena hanya ada satu kasus yang tersisa.)

Untuk pemeriksaan kesalahan dasar, ini sepertinya bukan peningkatan dari beberapa nilai pengembalian, karena satu baris lebih panjang dan mendeklarasikan satu variabel lokal lagi. Namun, skalanya akan lebih baik ke banyak kasus (dengan menambahkan lebih banyak pernyataan if), dan kompiler dapat memeriksa apakah semua kasus ditangani.

@sirkon

Mereka memang tidak untuk sebagian besar kasus penggunaan Go: layanan dan utilitas jaringan sepele. Tapi begitu sistem tumbuh lebih besar, ada kemungkinan besar mereka berguna.
[…]
Maksud saya, jaminan sistem tipe Go agak terlalu lemah untuk sesuatu yang lebih kompleks daripada layanan jaringan primitif biasa.

Pernyataan seperti ini tidak perlu konfrontatif dan menghina. Mereka juga agak memalukan, TBH, karena ada layanan nontrivial yang sangat besar yang ditulis di Go. Dan mengingat sebagian besar pengembangnya bekerja di Google, Anda seharusnya berasumsi bahwa mereka tahu lebih baik dari Anda, jika cocok untuk menulis layanan besar dan non-sepele. Go mungkin tidak mencakup semua kasus penggunaan (juga seharusnya, IMO), tetapi secara empiris tidak hanya berfungsi untuk "layanan jaringan primitif".

Penanganan NPD dapat dilakukan dengan cara yang serupa

Saya pikir ini benar-benar menggambarkan bahwa pendekatan Anda sebenarnya tidak menambah nilai signifikan. Seperti yang Anda tunjukkan, itu hanya menambahkan sintaks yang berbeda untuk dereference. Tapi AFAICT tidak ada yang mencegah programmer menggunakan sintaks itu pada nilai nol (yang mungkin masih panik). yaitu setiap program yang valid menggunakan *p juga valid menggunakan p.T (atau p.default ? Sulit untuk mengatakan apa ide Anda secara spesifik) dan sebaliknya.

Satu-satunya keuntungan yang dapat ditambahkan oleh tipe penjumlahan pada penanganan kesalahan dan dereferensi nihil adalah bahwa kompiler dapat memaksakan bahwa Anda harus membuktikan bahwa operasi tersebut aman dengan pencocokan pola di atasnya. Sebuah proposal yang menghilangkan bahwa penegakan tampaknya tidak membawa hal-hal baru yang signifikan ke meja (bisa dibilang, itu lebih buruk daripada menggunakan jumlah terbuka melalui interface), sedangkan proposal yang tidak termasuk itu adalah apa yang Anda gambarkan sebagai "jelek".

@Merovius

Dan mengingat sebagian besar pengembangnya bekerja di Google, Anda harus berasumsi bahwa mereka lebih tahu dari Anda,

Berbahagialah orang-orang yang beriman.

Seperti yang Anda tunjukkan, itu hanya menambahkan sintaks yang berbeda untuk dereference.

lagi

var written int64
...
res := os.Stdout.Write(data) // Write([]byte) -> Result[int64, string] ≈ !int64
written += res // Will not compile as res is a packed result type
if !res {
    // we are living on non-default res branch thus the only choice left is the default
    return Result.Error(...)
}
written += res // is OK

@skybrian

ferr := os.Open(...)

variabel perantara inilah yang memaksa saya untuk meninggalkan ide ini. Seperti yang Anda lihat, pendekatan saya khusus untuk penanganan kesalahan dan nihil. Tugas-tugas kecil ini terlalu penting dan pantas mendapat perhatian khusus IMO.

@sirkon Anda tampaknya memiliki minat yang sangat kecil untuk berbicara langsung dengan orang-orang. Aku akan berhenti di situ.

Mari kita menjaga percakapan kita tetap sopan, dan menghindari komentar yang tidak membangun. Kita boleh berselisih pendapat tentang berbagai hal, tetapi tetap mempertahankan wacana yang terhormat. https://golang.org/conduct.

Dan mengingat sebagian besar pengembangnya bekerja di Google, Anda seharusnya berasumsi bahwa mereka lebih tahu daripada Anda

Saya ragu Anda bisa membuat argumen seperti itu di Google.

@hasufell orang itu berasal dari Jerman di mana mereka tidak memiliki perusahaan IT besar dengan wawancara omong kosong untuk memompa ego pewawancara dan manajemen raksasa, itu sebabnya kata-kata ini.

@sirkon hal yang sama berlaku untuk Anda. Argumen ad-hominem dan sosial tidak berguna. Ini lebih dari masalah CoC. Saya telah melihat "argumen sosial" semacam ini muncul lebih sering ketika itu tentang bahasa inti: pengembang kompiler tahu lebih baik, perancang bahasa lebih tahu, orang-orang google lebih tahu.

Tidak, tidak. Tidak ada otoritas intelektual. Hanya ada otoritas keputusan. Lupakan saja.

Menyembunyikan beberapa komentar untuk mengatur ulang percakapan (dan terima kasih @agnivade telah mencoba mengembalikannya ke jalur).

Teman-teman, mohon pertimbangkan peran Anda dalam diskusi ini dengan mempertimbangkan nilai-nilai Gopher kita: setiap orang di komunitas memiliki perspektif untuk dibawa, dan kita harus berusaha untuk menghormati dan beramal dalam cara kita menafsirkan dan menanggapi satu sama lain.

Izinkan saya, tolong, untuk menambahkan 2 sen saya ke diskusi ini:

Kami membutuhkan cara untuk mengelompokkan tipe yang berbeda bersama-sama berdasarkan fitur selain kumpulan metodenya (seperti halnya antarmuka). Fitur pengelompokan baru harus memungkinkan menyertakan tipe primitif (atau dasar), yang tidak memiliki metode apa pun, dan tipe antarmuka untuk dikategorikan sebagai serupa secara relevan. Kita dapat mempertahankan tipe primitif (boolean, numerik, string, dan bahkan []byte, []int, dll.) sebagaimana adanya tetapi mengaktifkan abstraksi dari perbedaan antar tipe di mana definisi tipe mengelompokkannya dalam sebuah keluarga.

Saya sarankan kita menambahkan sesuatu seperti konstruksi tipe _family_ ke bahasa.

Sintaks

Keluarga tipe dapat didefinisikan seperti tipe lainnya:

type theFamilyName family {
    someType
    anotherType
}

Sintaks formal akan menjadi seperti:
FamilyType = "family" "{" { TypeName ";" } "}" .

Keluarga tipe dapat didefinisikan di dalam tanda tangan fungsi:

func Display(s family{string; fmt.Stringer}) { /* function body */ }

Artinya, definisi satu baris membutuhkan titik koma di antara nama tipe.

Nilai nol dari tipe keluarga adalah nihil, seperti dengan antarmuka nil.

(Di bawah tenda, nilai yang berada di belakang abstraksi keluarga diimplementasikan seperti antarmuka.)

Penalaran

Kami membutuhkan sesuatu yang lebih tepat daripada antarmuka kosong di mana kami ingin menentukan tipe apa yang valid sebagai argumen untuk suatu fungsi atau sebagai pengembalian suatu fungsi.

Solusi yang diusulkan akan memungkinkan keamanan tipe yang lebih baik, diperiksa sepenuhnya pada waktu kompilasi dan tidak menambahkan overhead tambahan saat runtime.

Intinya _Go code harus lebih self-documenting_. Apa yang dapat diambil fungsi sebagai argumen harus dibangun ke dalam kode itu sendiri.

Terlalu banyak kode yang salah mengeksploitasi fakta bahwa "antarmuka{} tidak mengatakan apa-apa." Agak memalukan bahwa konstruksi yang banyak digunakan (dan disalahgunakan) di Go, yang tanpanya kami tidak dapat berbuat banyak, kata _nothing_.

Beberapa contoh

Dokumentasi untuk fungsi sql.Rows.Scan menyertakan blok besar yang merinci jenis apa yang dapat diteruskan ke fungsi:

Scan converts columns read from the database into the following common Go types and special types provided by the sql package:
 *string
 *[]byte
 *int, *int8, *int16, *int32, *int64
 *uint, *uint8, *uint16, *uint32, *uint64
 *bool
 *float32, *float64
 *interface{}
 *RawBytes
 any type implementing Scanner (see Scanner docs)

Dan untuk fungsi sql.Row.Scan dokumentasi menyertakan kalimat “Lihat dokumentasi di Rows.Scan for details.” Lihat dokumentasi untuk _beberapa fungsi lain_ untuk detailnya? Ini bukan Go-like—dan dalam hal ini kalimat tersebut tidak benar karena sebenarnya Rows.Scan dapat mengambil nilai *RawBytes tetapi Row.Scan tidak bisa.

Masalahnya adalah kita sering dipaksa untuk mengandalkan komentar untuk jaminan dan kontrak perilaku, yang tidak dapat ditegakkan oleh kompiler.

Ketika dokumen untuk suatu fungsi mengatakan bahwa fungsi tersebut bekerja seperti beberapa fungsi lainnya—“jadi lihat dokumentasi untuk fungsi lain itu”—Anda hampir dapat menjamin bahwa fungsi tersebut kadang-kadang akan disalahgunakan. Saya berani bertaruh bahwa kebanyakan orang, seperti saya, hanya mengetahui bahwa *RawBytes tidak diizinkan sebagai argumen di Row.Scan hanya setelah mendapatkan kesalahan dari Row.Scan ( mengatakan "sql: RawBytes tidak diizinkan di Row.Scan"). Sangat menyedihkan bahwa sistem tipe mengizinkan kesalahan seperti itu.

Kita bisa memiliki:

type Value family {
    *string
    *[]byte
    *int; *int8; *int16; *int32; *int64
    *uint; *uint8; *uint16; *uint32; *uint64
    *bool
    *float32; *float64
    *interface{}
    *RawBytes
    Scanner
}

Dengan cara ini, nilai yang diteruskan harus menjadi salah satu tipe dalam keluarga yang diberikan, dan sakelar tipe di dalam fungsi Rows.Scan tidak perlu menangani kasus yang tidak terduga atau default; akan ada keluarga lain untuk fungsi Row.Scan .

Pertimbangkan juga bagaimana struct cloud.google.com/go/datastore.Property memiliki bidang "Nilai" dengan tipe interface{} dan memerlukan semua dokumentasi ini:

// Value is the property value. The valid types are:
// - int64
// - bool
// - string
// - float64
// - *Key
// - time.Time
// - GeoPoint
// - []byte (up to 1 megabyte in length)
// - *Entity (representing a nested struct)
// Value can also be:
// - []interface{} where each element is one of the above types
// This set is smaller than the set of valid struct field types that the
// datastore can load and save. A Value's type must be explicitly on
// the list above; it is not sufficient for the underlying type to be
// on that list. For example, a Value of "type myInt64 int64" is
// invalid. Smaller-width integers and floats are also invalid. Again,
// this is more restrictive than the set of valid struct field types.
//
// A Value will have an opaque type when loading entities from an index,
// such as via a projection query. Load entities into a struct instead
// of a PropertyLoadSaver when using a projection query.
//
// A Value may also be the nil interface value; this is equivalent to
// Python's None but not directly representable by a Go struct. Loading
// a nil-valued property into a struct will set that field to the zero
// value.

Ini bisa jadi:

type PropertyVal family {
  int64
  bool
  string
  float64
  *Key
  time.Time
  GeoPoint
  []byte
  *Entity
  nil
  []int64; []bool; []string; []float64; []*Key; []time.Time; []GeoPoint; [][]byte; []*Entity
}

(Anda dapat membayangkan bagaimana ini dapat dibagi menjadi dua keluarga.)

Jenis json.Token disebutkan di atas. Definisi tipenya adalah:

type Token family {
    Delim
    bool
    float64
    Number
    string
    nil
}

Contoh lain yang saya dapatkan baru-baru ini:
Saat memanggil fungsi seperti sql.DB.Exec , atau sql.DB.Query , atau fungsi apa pun yang menggunakan daftar variadik interface{} mana setiap elemen harus memiliki tipe dalam set tertentu dan _tidak sendiri a slice_, penting untuk diingat untuk menggunakan operator "spread" saat meneruskan argumen dari []interface{} ke fungsi seperti itu: salah untuk mengatakan DB.Exec("some query with placeholders", emptyInterfaceSlice) ; cara yang benar adalah: DB.Exec("the query...", emptyInterfaceSlice...) mana emptyInterfaceSlice memiliki tipe []interface{} . Cara elegan untuk membuat kesalahan seperti itu menjadi tidak mungkin adalah dengan membuat fungsi ini mengambil argumen variadik Value , di mana Value didefinisikan sebagai keluarga seperti yang dijelaskan di atas.

Inti dari contoh-contoh ini adalah bahwa _kesalahan nyata sedang dibuat_ karena ketidaktepatan interface{} .

var x int | float64 | string | rune
z = int(x) + int(y)
z = float64(x) + float64(y)

Ini pasti kesalahan kompiler karena jenis x tidak benar-benar kompatibel dengan apa yang dapat diteruskan ke int() .

Saya suka ide memiliki family . Ini pada dasarnya akan menjadi antarmuka yang dibatasi (dibatasi?) Untuk jenis yang terdaftar dan kompiler dapat memastikan Anda cocok dengan semua waktu dan mengubah jenis variabel dalam konteks lokal dari case .

Masalahnya adalah kita sering terpaksa mengandalkan komentar untuk jaminan dan
kontrak perilaku, yang tidak dapat ditegakkan oleh kompiler.

Itulah sebenarnya alasan mengapa saya mulai sedikit tidak menyukai hal-hal seperti

func foo() (..., error) 

karena Anda tidak tahu jenis kesalahan apa yang dikembalikan.

dan beberapa hal lain yang mengembalikan antarmuka alih-alih tipe konkret. Beberapa fungsi
return net.Addr dan terkadang agak sulit untuk menggali kode sumber untuk mencari tahu jenis net.Addr yang sebenarnya dikembalikan dan kemudian menggunakannya dengan tepat. Tidak ada banyak kerugian dalam mengembalikan tipe beton (karena mengimplementasikan antarmuka dan dengan demikian dapat digunakan di mana saja di mana antarmuka dapat digunakan) kecuali ketika Anda
nanti rencanakan untuk memperluas metode Anda untuk mengembalikan jenis net.Addr . Tapi jika Anda
API menyebutkan itu mengembalikan OpError lalu mengapa tidak menjadikannya bagian dari spesifikasi "waktu kompilasi"?

Sebagai contoh:

 OpError is the error type usually returned by functions in the net package. It describes the operation, network type, and address of an error. 

Biasanya? Tidak memberi tahu Anda dengan tepat fungsi mana yang mengembalikan kesalahan ini. Dan ini adalah dokumentasi untuk jenisnya, bukan fungsinya. Dokumentasi untuk Read tidak menyebutkan bahwa ia mengembalikan OpError. Juga, jika Anda melakukannya

err := blabla.(*OpError)

itu akan macet setelah mengembalikan jenis kesalahan yang berbeda. Itu sebabnya saya sangat ingin melihat ini sebagai bagian dari deklarasi fungsi. Setidaknya *OpError | error akan memberi tahu Anda bahwa itu kembali
kesalahan seperti itu dan kompiler memastikan Anda tidak melakukan pernyataan tipe yang tidak dicentang yang membuat program Anda mogok di masa mendatang.

BTW: Apakah sistem seperti polimorfisme tipe Haskell sudah dipertimbangkan? Atau sistem tipe berbasis 'sifat' yaitu:

func calc(a < add(a, a) a >, b a) a {
   return add(a, b)
}

func drawWidgets(widgets []< widgets.draw() error >) error {
  for _, widgets := range widgets {
    err := widgets.draw()
    if err != nil {
      return err
    }
  }
  return nil
}

a < add(a, a) a artinya "apapun tipe a, pasti ada fungsi add(typeof a, typeof a) typeof a)". < widgets.draw() error> berarti "jenis widget apa pun, harus menyediakan penarikan metode yang mengembalikan kesalahan". Ini akan memungkinkan lebih banyak fungsi umum dibuat:

func Sum(a []< add(a,a) a >) a {
  sum := a[0]
  for i := 1; i < len(a); i++ {
    sum = add(sum,a[i])
  }
  return sum
}

(Perhatikan bahwa ini tidak sama dengan "generik" tradisional).

Tidak ada banyak kerugian dalam mengembalikan tipe konkret (karena mengimplementasikan antarmuka dan dengan demikian dapat digunakan di mana saja di mana antarmuka dapat digunakan) kecuali ketika Anda kemudian berencana untuk memperluas metode Anda untuk mengembalikan jenis net.Addr .

Juga, Go tidak memiliki subtipe varian, jadi Anda tidak dapat menggunakan func() *FooError sebagai func() error jika diperlukan. Yang sangat penting untuk kepuasan antarmuka. Dan terakhir, ini tidak dikompilasi:

func Foo() (FooVal, FooError) {
    // ...
}

func Bar(f FooVal) (BarVal, BarError) {
    // ...
}

func main() {
    foo, err := Foo()
    if err != nil {
        log.Fatal(err)
    }
    bar, err := Bar(foo) // Type error: Can not assign BarError to err (type FooError)
    if err != nil {
        log.Fatal(err)
    }
}

yaitu untuk membuat ini bekerja (saya ingin jika kita bisa entah bagaimana) kita membutuhkan inferensi tipe yang jauh lebih canggih - saat ini, Go hanya menggunakan informasi tipe lokal dari satu ekspresi. Dalam pengalaman saya, algoritme inferensi tipe semacam itu tidak hanya secara signifikan lebih lambat (memperlambat kompilasi dan biasanya runtime tidak dibatasi) tetapi juga menghasilkan pesan kesalahan yang jauh lebih mudah dipahami.

Selain itu, Go tidak memiliki subtipe varian, jadi Anda tidak dapat menggunakan func() *FooError sebagai kesalahan func() jika diperlukan. Yang sangat penting untuk kepuasan antarmuka. Dan terakhir, ini tidak dikompilasi:

Saya berharap ini berfungsi dengan baik di Go tetapi saya tidak pernah menemukan ini karena praktik saat ini hanya menggunakan error . Tapi ya, dalam kasus ini pembatasan ini secara praktis memaksa Anda untuk menggunakan error sebagai tipe pengembalian.

func main() {
    foo, err := Foo()
    if err != nil {
        log.Fatal(err)
    }
    bar, err := Bar(foo) // Type error: Can not assign BarError to err (type FooError)
    if err != nil {
        log.Fatal(err)
    }
}

Saya tidak mengetahui bahasa apa pun yang memungkinkan ini (well, kecuali untuk bahasa esolang) tetapi yang harus Anda lakukan hanyalah menyimpan "jenis dunia" (yang pada dasarnya adalah peta variable -> type ) dan jika Anda kembali -tetapkan variabel yang baru saja Anda perbarui tipenya di "dunia tipe".

Saya tidak berpikir Anda memerlukan inferensi tipe yang rumit untuk melakukan ini tetapi Anda perlu melacak jenis variabel tetapi saya berasumsi Anda tetap perlu melakukannya karena

var int i = 0;
i = "hi";

Anda pasti entah bagaimana harus mengingat variabel/deklarasi mana yang memiliki tipe dan untuk i = "hi" Anda perlu membuat "pencarian tipe" pada i untuk memeriksa apakah Anda dapat menetapkan string untuk itu.

Apakah ada masalah praktis yang memperumit penetapan func () *ConcreteError ke func() error selain pemeriksa tipe yang tidak mendukungnya (seperti alasan runtime/alasan kode yang dikompilasi)? Saya kira saat ini Anda harus membungkusnya dalam fungsi seperti ini:

type MyFunc func() error

type A struct {
}

func (_ *A) Error() string { return "" }

func NewA() *A {
    return &A{}
}

func main() {
    var err error = &A{}
    fmt.Println(err.Error())
    var mf MyFunc = MyFunc(func() error { return NewA() }) // type checks fine
        //var mf MyFunc = MyFunc(NewA) // doesn't type check
    _ = mf
}

Jika Anda dihadapkan dengan func (a, b) c tetapi mendapatkan func (x, y) z yang perlu dilakukan adalah memeriksa apakah z dapat ditetapkan ke c (dan a , b harus dapat ditetapkan ke x , y ) yang setidaknya pada level tipe tidak melibatkan inferensi tipe yang rumit (ini hanya melibatkan pemeriksaan apakah suatu tipe dapat ditetapkan/kompatibel dengan/dengan tipe lain). Tentu saja, apakah ini menyebabkan masalah dengan runtime/kompilasi ... Saya tidak tahu, tetapi setidaknya secara ketat melihat level tipe, saya tidak mengerti mengapa ini melibatkan inferensi tipe yang rumit. Pemeriksa tipe sudah mengetahui apakah x dapat ditetapkan ke a sehingga ia juga dengan mudah mengetahui apakah func () x dapat ditetapkan ke func () a . Tentu saja, mungkin ada alasan praktis (berpikir tentang representasi runtime) mengapa ini tidak mudah dilakukan. (Saya menduga itulah inti sebenarnya di sini, bukan pemeriksaan tipe yang sebenarnya).

Secara teoritis Anda dapat mengatasi masalah runtime (jika ada) dengan membungkus fungsi secara otomatis (seperti pada cuplikan di atas) dengan kelemahan _berpotensi besar_ yang mengacaukan perbandingan fungsi dengan fungsi (karena fungsi yang dibungkus tidak akan sama dengan fungsi itu membungkus).

Saya tidak mengetahui bahasa apa pun yang memungkinkan ini (well, kecuali untuk esolang)

Tidak persis, tetapi saya berpendapat itu karena bahasa dengan sistem tipe yang kuat biasanya adalah bahasa fungsional yang tidak benar-benar menggunakan variabel (dan karenanya tidak benar-benar membutuhkan kemampuan untuk menggunakan kembali pengidentifikasi). FWIW, saya berpendapat bahwa misalnya sistem tipe Haskell akan dapat menangani ini dengan baik - setidaknya selama Anda tidak menggunakan properti lain dari FooError atau BarError , seharusnya dapat menyimpulkan bahwa err bertipe error dan menanganinya. Tentu saja, sekali lagi, ini hipotetis, karena situasi yang tepat ini tidak mudah ditransfer ke bahasa fungsional.

tapi saya berasumsi Anda tetap perlu melakukan itu karena

Perbedaannya adalah, dalam contoh Anda, i memiliki tipe yang jelas dan dipahami dengan baik setelah baris pertama, yaitu int dan Anda kemudian mengalami kesalahan tipe ketika Anda menetapkan string untuk itu. Sementara itu, untuk sesuatu seperti yang saya sebutkan, setiap penggunaan pengidentifikasi pada dasarnya menciptakan serangkaian batasan pada tipe yang digunakan dan pemeriksa tipe kemudian mencoba menyimpulkan tipe paling umum yang memenuhi semua batasan yang diberikan (atau mengeluh bahwa tidak ada tipe yang memenuhi itu kontrak). Untuk itulah teori tipe formal.

Apakah ada masalah praktis yang memperumit penetapan func () *ConcreteError ke func() error selain pemeriksa tipe yang tidak mendukungnya (seperti alasan runtime/alasan kode yang dikompilasi)?

Ada masalah praktis, tetapi saya percaya untuk func mereka mungkin dapat dipecahkan (dengan memancarkan kode un/-wrapping, mirip dengan cara kerja antarmuka-passing). Saya menulis sedikit tentang varians di Go dan menjelaskan beberapa masalah praktis yang saya lihat di bagian bawah. Saya tidak sepenuhnya yakin itu layak ditambahkan. Yaitu saya tidak yakin itu memecahkan masalah penting sendiri.

dengan potensi kerugian besar yang mengacaukan perbandingan fungsi dengan fungsi (karena fungsi yang dibungkus tidak akan sama dengan fungsi yang dibungkusnya).

fungsinya tidak sebanding.

Bagaimanapun, TBH, semua ini tampaknya agak di luar topik untuk masalah ini :)

FYI: Saya baru saja melakukan ini . Ini tidak bagus, tapi pasti aman untuk tipe. (Hal yang sama dapat dilakukan untuk #19814 FWIW)

Saya agak terlambat ke pesta, tetapi saya juga ingin berbagi dengan Anda perasaan saya setelah 4 tahun Go:

  • Pengembalian multi-nilai adalah kesalahan besar.
  • Antarmuka yang tidak dapat digunakan adalah kesalahan.
  • Pointer bukan sinonim untuk "opsional", serikat pekerja yang didiskriminasi seharusnya digunakan sebagai gantinya.
  • Unmarshaller JSON seharusnya mengembalikan kesalahan jika bidang wajib tidak disertakan dalam dokumen JSON.

Dalam 4 tahun terakhir saya telah menemukan banyak masalah yang terkait dengannya:

  • data sampah kembali jika terjadi kesalahan.
  • kekacauan sintaks (mengembalikan nilai nol jika terjadi kesalahan).
  • pengembalian multi-kesalahan (API yang membingungkan, tolong, jangan lakukan itu!).
  • antarmuka non-nil yang menunjuk ke pointer yang menunjuk ke nil (membingungkan orang-orang yang membuat pernyataan "Pergi adalah bahasa yang mudah" terdengar seperti lelucon yang buruk).
  • bidang JSON yang tidak dicentang membuat server macet (yey!).
  • pointer yang dikembalikan tidak dicentang membuat server macet, namun tidak ada yang mendokumentasikan pointer yang dikembalikan mewakili opsional (mungkin tipe) dan oleh karena itu, bisa menjadi nil (yey!)

Perubahan yang diperlukan untuk memperbaiki semua masalah itu, bagaimanapun, akan membutuhkan versi Go 2.0.0 (bukan Go2) yang benar-benar tidak kompatibel, yang saya kira tidak akan pernah terwujud. Bagaimanapun...

Beginilah seharusnya penanganan kesalahan terlihat:

// Divide returns either a float64 or an arbitrary error
func Divide(dividend, divisor float64) float64 | error {
  if dividend == 0 {
    return errors.New("dividend is zero")
  }
  if divisor == 0 {
    return errors.New("divisor is zero")
  }
  return dividend / divisor
}

func main() {
  // type-switch statements enforce completeness:
  switch v := Divide(1, 0).(type) {
  case float64:
    log.Print("1/0 = ", v)
  case error:
    log.Print("1/0 = error: ", v)
  }

  // type-assertions, however, do not:
  divisionResult := Divide(3, 1)
  if v, ok := divisionResult.(float64); ok {
    log.Print("3/1 = ", v)
  }
  if v, ok := divisionResult.(error); ok {
    log.Print("3/1 = error: ", v.Error())
  }
  // yet they don't allow asserting types not included in the union:
  if v, ok := divisionResult.(string); ok { // compile-time error!
    log.Print("3/1 = string: ", v)
  }
}

Antarmuka bukan pengganti serikat pekerja yang didiskriminasi , mereka adalah dua hewan yang sama sekali berbeda. Kompilator memastikan sakelar tipe pada serikat yang didiskriminasi sudah lengkap, artinya kasing mencakup semua tipe yang mungkin, jika Anda tidak menginginkan ini maka Anda dapat menggunakan pernyataan pernyataan tipe.

Terlalu sering saya melihat orang yang benar-benar bingung tentang _antarmuka non-nil ke nilai nil_ : https://play.golang.org/p/JzigZ2Q6E6F. Biasanya, orang menjadi bingung ketika antarmuka error menunjuk ke pointer dari jenis kesalahan khusus yang menunjuk ke nil , itulah salah satu alasan saya pikir membuat antarmuka nil-able adalah sebuah kesalahan.

Antarmuka seperti resepsionis, Anda tahu itu manusia ketika Anda berbicara dengannya, tetapi di Go, itu bisa menjadi sosok kardus dan dunia akan tiba-tiba runtuh jika Anda mencoba berbicara dengannya.

Diskriminasi serikat pekerja seharusnya digunakan untuk opsional (mungkin tipe) dan meneruskan nil pointer ke antarmuka seharusnya menghasilkan kepanikan:

type CustomErr struct {}
func (err *CustomErr) Error() string { return "custom error" }

func CouldFail(foo int) error | nil {
  var err *customErr
  if foo > 10 {
    // you can't return a nil pointer as an interface value
    return err // this will panic!
  }
  // no error
  return nil
}

func main() {
  // assume no error
  if err, ok := CouldFail().(error); ok {
    log.Fatalf("it failed, Jim! %s", err)
  }
}

Pointer dan tipe-mungkin tidak dapat dipertukarkan. Menggunakan pointer untuk tipe opsional itu buruk karena mengarah ke API yang membingungkan:

// P returns a pointer to T, but it's not clear whether or not the pointer
// will always reference a T instance. It might be an optional T,
// but the documentation usually doesn't tell you.
func P() *T {}

// O returns either a pointer to T or nothing, this implies (but still doesn't guarantee)
// that the pointer is always expected to not be nil, in any other case nil is returned.
func O() *T | nil {}

Lalu ada juga JSON. Ini tidak akan pernah terjadi dengan serikat pekerja karena kompiler memaksa Anda untuk memeriksanya sebelum digunakan . Unmarshaller JSON akan gagal jika bidang wajib (termasuk bidang tipe penunjuk) tidak disertakan dalam dokumen JSON:

type DataModel struct {
  // Optional needs to be type-checked before use
  // and is therefore allowed to no be included in the JSON document
  Optional string | nil `json:"optional,omitempty"`
  // Required won't ever be nil
  // If the JSON document doesn't include it then unmarshalling will return an error
  Required *T `json:"required"`
}

PS
Saya juga sedang mengerjakan desain bahasa fungsional saat ini dan ini adalah bagaimana saya menggunakan serikat yang didiskriminasi untuk penanganan kesalahan di sana:

read = (s String) -> (Array<Byte> or Error) => match s {
  "A" then Error<NotFound>
  "B" then Error<AccessDenied>
  "C" then Error<MemoryLimitExceeded>
  else Array<Byte>("this is fine")
}

main = () -> ?Error => {
  // assume the result is a byte array
  // otherwise throw the error up the stack wrapped in a "type-assertion-failure" error
  r = read("D") as Array<Byte>
  log::print("data: %s", r)
}

Saya akan senang melihat ini menjadi kenyataan suatu hari nanti. Jadi mari kita lihat apakah saya dapat membantu sedikit:

Mungkin masalahnya adalah kami mencoba menutupi terlalu banyak dengan proposal. Kita bisa menggunakan versi yang disederhanakan yang membawa sebagian besar nilai sehingga akan lebih mudah untuk menambahkannya ke bahasa dalam jangka pendek.

Dari sudut pandang saya, versi yang disederhanakan ini

  1. Hanya izinkan | versi
    <any pointer type> | nil
    Di mana semua jenis pointer akan menjadi: pointer, fungsi, saluran, irisan, dan peta (tipe pointer Go)
  2. Melarang menetapkan nil ke tipe pointer kosong. Jika Anda ingin menetapkan nil, maka tipenya harus <pointer type> | nil . Sebagai contoh:
var n *int       = nil // Does not compile, wrong type
var n *int | nil = nil // Ok!

var set map[string] bool       = nil // Does not compile
var set map[string] bool | nil = nil // Ok!

var myFunc func(int) err       = nil // Nope!
var myFunc func(int) err | nil = nil // All right.

Itu adalah ide-ide utamanya. Berikut ini adalah ide-ide turunan dari yang utama:

  1. Anda tidak dapat mendeklarasikan variabel tipe pointer kosong dan membiarkannya tidak diinisialisasi. Jika Anda ingin melakukan itu, maka Anda perlu menambahkan jenis diskriminasi | nil
var maybeAString *string       // Wrong: invalid initial value
var maybeAString *string | nil // Good
  1. Anda dapat menetapkan tipe pointer kosong ke tipe pointer "nilable", tetapi tidak sebaliknya:
var value int = 42
var barePointer *int = &value          // Valid
var nilablePointer *int | nil = &value // Valid

nilablePointer = barePointer // Valid
barePointer = nilablePointer // Invalid: Incompatible types
  1. Satu- satunya cara untuk mendapatkan nilai dari tipe pointer "nilable" adalah melalui sakelar tipe, seperti yang ditunjukkan orang lain. Misalnya, mengikuti contoh di atas, jika kita benar-benar ingin menetapkan nilai nilablePointer ke barePointer , maka kita perlu melakukan:
switch val := nilablePointer.(type) {
  case *int:
    barePointer = val // Yeah! Types are compatible now. It is imposible that "val = nil"
  case nil:
    // Do what you need to do when nilablePointer is nil
}

Dan itu saja. Saya tahu bahwa serikat pekerja yang didiskriminasi dapat digunakan untuk lebih banyak lagi (terutama dalam kasus kesalahan pengembalian), tetapi saya akan mengatakan bahwa berpegang teguh pada apa yang saya tulis di atas kita akan membawa nilai BESAR ke bahasa dengan sedikit usaha dan tanpa memperumitnya lebih dari yang diperlukan.
Manfaat yang saya lihat dengan proposal sederhana ini:

  • a) Tidak ada kesalahan penunjuk nol . Oke, tidak pernah 4 kata berarti sebanyak itu. Itulah mengapa saya merasa perlu untuk mengatakannya dari sudut pandang lain: Program No Go akan _EVER_ memiliki kesalahan nil pointer dereference lagi! 💥
  • b) Anda dapat meneruskan pointer ke parameter fungsi tanpa memperdagangkan "kinerja vs niat" .
    Maksud saya dengan ini adalah bahwa ada beberapa waktu ketika saya ingin meneruskan struct ke suatu fungsi, dan bukan pointer ke sana, karena saya tidak ingin fungsi itu khawatir tentang nullity dan memaksanya untuk memeriksa parameter . Namun, saya biasanya melewati pointer untuk menghindari overhead penyalinan.
  • c) Tidak ada lagi peta nihil! YA! Kami akan mengakhiri dengan ketidakkonsistenan tentang "slice nil yang aman" dan "peta nil yang tidak aman" (yang akan membuat panik jika Anda mencoba menulis kepada mereka). Peta akan diinisialisasi atau bertipe map | nil , dalam hal ini Anda perlu menggunakan sakelar tipe

Tetapi ada juga hal lain yang tidak berwujud di sini yang membawa banyak nilai: ketenangan pikiran pengembang . Anda dapat bekerja dan bermain dengan pointer, fungsi, saluran, peta, dll dengan perasaan santai bahwa Anda tidak perlu khawatir tentang mereka menjadi nihil. _Saya akan membayar untuk ini!_ 😂

Manfaat memulai dengan versi proposal yang lebih sederhana ini adalah bahwa hal itu tidak akan menghentikan kita untuk mengajukan proposal lengkap di masa mendatang, atau bahkan melangkah selangkah demi selangkah (menjadi, bagi saya, langkah alami berikutnya untuk memungkinkan pengembalian kesalahan yang didiskriminasi , tapi mari kita lupakan itu sekarang).

Satu masalah adalah bahwa bahkan versi proposal yang sederhana ini tidak kompatibel ke belakang, tetapi dapat dengan mudah diperbaiki dengan gofix : cukup ganti semua deklarasi tipe pointer dengan <pointer type> | nil .

Bagaimana menurutmu? Saya harap ini bisa menjelaskan dan mempercepat dimasukkannya nil-safety ke dalam bahasa. Tampaknya cara ini (melalui "serikat-serikat yang terdiskriminasi") adalah cara yang lebih sederhana dan lebih ortogonal untuk mencapainya.

@alvaroloes

Anda tidak dapat mendeklarasikan variabel tipe pointer kosong dan membiarkannya tidak diinisialisasi.

Ini adalah inti dari masalah ini. Itu bukan hal yang dilakukan Go - setiap jenis memiliki nilai nol, titik. Kalau tidak, Anda harus menjawab apa, misalnya make([]T, 100) tidak? Hal-hal lain yang Anda sebutkan (mis. peta nihil panik saat menulis) adalah konsekuensi dari aturan dasar ini. (Dan sebagai tambahan, saya tidak berpikir itu benar untuk mengatakan bahwa nil-slice lebih aman daripada peta - menulis ke nil-slice akan panik sama seperti menulis ke nil-peta).

Dengan kata lain: Proposal Anda sebenarnya tidak sesederhana itu, karena sangat menyimpang dari keputusan desain yang cukup mendasar dalam bahasa Go.

Saya pikir hal yang lebih penting yang dilakukan Go adalah membuat nilai nol menjadi berguna dan tidak hanya memberikan semua nilai nol. Peta nihil adalah nilai nol tetapi tidak berguna. Itu berbahaya, sebenarnya. Jadi mengapa tidak melarang nilai nol jika itu tidak berguna. Mengubah Go dalam hal ini akan bermanfaat tetapi proposalnya memang tidak sesederhana itu.

Proposal di atas lebih mirip hal opsional/non-opsional seperti di Swift dan lainnya. Ini keren dan semuanya kecuali:

  1. Itu akan merusak hampir semua program di luar sana dan perbaikannya tidak akan sepele untuk gofix. Anda tidak bisa hanya mengganti semuanya dengan <pointer type> | nil karena, per proposal, ini akan memerlukan sakelar tipe untuk membongkar nilainya.
  2. Agar ini benar-benar dapat digunakan dan ditanggung, Go perlu memiliki lebih banyak gula sintaksis di sekitar opsi ini. Ambil Swift, misalnya. Ada banyak fitur dalam bahasa khusus untuk bekerja dengan opsional - penjaga, pengikatan opsional, rantai opsional, penggabungan nihil dll. Saya tidak berpikir Go akan pergi ke arah itu tetapi tanpa mereka bekerja dengan opsional akan menjadi tugas.

Jadi mengapa tidak melarang nilai nol jika itu tidak berguna.

Lihat di atas. Ini berarti bahwa beberapa hal yang terlihat murah memiliki biaya yang sangat non-sepele yang terkait dengannya.

Mengubah Go dalam hal ini akan bermanfaat

Ini memiliki manfaat, tetapi itu tidak sama dengan bermanfaat. Ini juga memiliki kerugian. Yang berbobot lebih berat terserah preferensi dan tradeoff. Para desainer Go memilih ini.

FTR, ini adalah pola umum di utas ini dan salah satu argumen tandingan utama untuk konsep tipe penjumlahan apa pun - bahwa Anda perlu mengatakan apa nilai nolnya. Itulah sebabnya setiap ide baru harus secara eksplisit membahasnya. Tapi agak frustasi, kebanyakan orang yang posting di sini hari ini belum membaca sisa thread dan cenderung mengabaikan bagian itu.

Ah! Saya tahu ada sesuatu yang jelas saya lewatkan. Doh! Kata "sederhana" memiliki arti yang kompleks. Ok, jangan ragu untuk menghapus kata "sederhana" dari komentar saya sebelumnya.

Maaf jika itu membuat frustrasi beberapa dari Anda. Niat saya adalah mencoba membantu sedikit. Saya mencoba untuk mengikuti utasnya, tetapi saya tidak punya banyak waktu luang untuk dihabiskan untuk ini.

Kembali ke masalah: jadi sepertinya alasan utama yang menahan ini adalah nilai nol.
Setelah berpikir sejenak dan membuang banyak pilihan, satu-satunya hal yang menurut saya dapat menambah nilai dan layak disebut adalah sebagai berikut:

Jika saya ingat dengan benar, nilai nol dari jenis apa pun terdiri dari mengisi ruang memorinya dengan 0.
Seperti yang sudah Anda ketahui, ini baik untuk tipe non-pointer, tetapi ini adalah sumber bug untuk tipe pointer:

type S struct {
    n int
}
var s S 
s.n  // Fine

var s *S
s.n // runtime error

var f func(int)
f() // runtime error

Jadi, bagaimana jika kita:

  • Tentukan nilai nol yang berguna untuk setiap jenis pointer
  • Inisialisasi hanya saat pertama kali digunakan (inisialisasi malas).

Saya pikir ini telah disarankan dalam masalah lain, tidak yakin. Saya hanya menulisnya di sini karena membahas titik hambatan utama dari proposal ini.

Berikut ini adalah daftar nilai nol untuk tipe pointer. Perhatikan bahwa nilai nol tersebut hanya akan digunakan

| Jenis penunjuk | Nilai nol | Nilai nol dinamis | Komentar |
| --- | --- | --- | --- |
| *T | nil | baru(T) |
| []T | nil | []T{} |
| peta[T]U | nil | peta[T]U{} |
| func | nil | sial | Jadi nilai nol dinamis dari suatu fungsi tidak melakukan apa-apa dan mengembalikan nilai nol. Jika daftar nilai kembalian selesai dalam error , maka kesalahan default dikembalikan dengan mengatakan bahwa fungsinya adalah "tidak ada operasi" |
| chan T | nil | buat(chan T) |
| interface | nil | - | implementasi default di mana semua metode diinisialisasi dengan fungsi noop dijelaskan di atas |
| serikat yang didiskriminasi | nil | nilai nol dinamis dari tipe pertama | |

Sekarang, ketika tipe-tipe itu diinisialisasi, mereka akan menjadi nil , seperti sekarang. Perbedaannya adalah pada saat nil diakses. Pada saat itu, nilai nol dinamis akan digunakan. Beberapa contoh:

type S struct {
    n int
}
var s *S
if s == nil { // true. Nothing different happens here
...
}
s.n = 1       // At this moment the go runtime would check if it is nil, and if it is, 
              // do "s = new(S)". We could say the code would be replaced by:
/*
if s == nil {
    s = new(S)
}
s.n = 1
*/

// -------------
var pointers []*S = make([]*S, 100) // Everything as usual
for _,p := range pointers {
    p.n = 1 // This is translated to:
    /*
        if p == nil {
            p = new(S)
        }
        p.n = 1
    */
}

// ------------
type I interface {
    Add(string) (int, error)
}

var i I
n, err := i.Add("yup!") // This method returns 0, and the default error "Noop"
if err != nil { // This condition is true and the error is returned
    return err
}

Saya mungkin kehilangan detail implementasi dan kemungkinan kesulitan, tetapi saya ingin fokus pada ide terlebih dahulu.

Kelemahan utama adalah kami menambahkan nil-check ekstra setiap kali Anda mengakses nilai dari tipe pointer. Tapi saya akan mengatakan:

  • Ini adalah tradeoff yang baik untuk manfaat yang kita dapatkan. Situasi yang sama terjadi dengan cek terikat dalam akses array/slice dan kami menerima pembayaran penalti kinerja itu untuk keamanan yang dibawanya.
  • Pemeriksaan nil dapat dihindari dengan cara yang sama seperti pemeriksaan terikat array: jika tipe pointer telah diinisialisasi dalam cakupan saat ini, kompiler dapat mengetahuinya dan menghindari penambahan pemeriksaan nil.

Dengan ini, kami memiliki semua manfaat yang dijelaskan dalam komentar sebelumnya, dengan plus bahwa kami tidak perlu menggunakan sakelar tipe untuk mengakses nilai (itu hanya untuk serikat yang didiskriminasi), menjaga kode go sebersih sekarang.

Bagaimana menurutmu? Mohon maaf jika ini sudah pernah dibahas. Juga, saya menyadari bahwa proposal komentar ini lebih terkait dengan nil daripada serikat pekerja yang didiskriminasi. Saya mungkin memindahkan ini ke masalah terkait nihil tetapi, seperti yang saya katakan, saya mempostingnya di sini karena mencoba memperbaiki masalah utama serikat pekerja yang didiskriminasi: nilai nol yang berguna.

Kembali ke masalah: jadi sepertinya alasan utama yang menahan ini adalah nilai nol.

Ini adalah salah satu alasan teknis yang signifikan yang perlu ditangani. Bagi saya, alasan utamanya adalah mereka membuat perbaikan bertahap sangat tidak mungkin (lihat di atas). yaitu bagi saya pribadi, ini bukan pertanyaan tentang bagaimana menerapkannya, tetapi pada dasarnya saya menentang konsep tersebut.
Bagaimanapun, alasan mana yang "utama" sebenarnya adalah masalah selera dan preferensi.

Jadi, bagaimana jika kita:

  • Tentukan nilai nol yang berguna untuk setiap jenis pointer
  • Inisialisasi hanya saat pertama kali digunakan (inisialisasi malas).

Ini gagal jika Anda meneruskan tipe pointer. misalnya

func F(p *T) {
    *p = 42 // same as if p == nil { p = new(T) } *p = 42
}

func G() {
    var p *T
    F(p)
    fmt.Println(p == nil) // Has to be true, as F can't modify p. But now F is silently misbehaving
}

Diskusi ini adalah segalanya tapi baru. Ada alasan mengapa tipe referensi berperilaku seperti itu dan bukan karena pengembang Go tidak memikirkannya :)

Ini adalah inti dari masalah ini. Itu bukan hal yang dilakukan Go - setiap jenis memiliki nilai nol, titik. Kalau tidak, Anda harus menjawab apa, misalnya make([]T, 100) tidak?

Ini (dan new(T) ) harus dilarang jika T tidak memiliki nilai nol. Anda harus melakukan make([]T, 0, 100) dan kemudian menggunakan append untuk mengisi irisan. Reslicing yang lebih besar ( v[:0][:100] ) juga harus menjadi kesalahan. [10]T pada dasarnya akan menjadi tipe yang tidak mungkin (kecuali kemampuan untuk menegaskan irisan ke penunjuk array ditambahkan ke bahasa). Dan Anda memerlukan cara untuk menandai tipe nilable yang ada sebagai non-nilable untuk mempertahankan kompatibilitas ke belakang.

Ini akan menimbulkan masalah jika obat generik ditambahkan, karena Anda harus memperlakukan semua parameter tipe sebagai tidak memiliki nilai nol kecuali mereka memenuhi beberapa batasan. Subset tipe juga membutuhkan pelacakan inisialisasi pada dasarnya di mana-mana. Ini akan menjadi perubahan yang cukup besar dengan sendirinya bahkan tanpa menambahkan tipe penjumlahan di atasnya. Ini tentu saja bisa dilakukan, tetapi berkontribusi secara signifikan pada sisi biaya dari analisis biaya/manfaat. Pilihan yang disengaja untuk menjaga inisialisasi tetap sederhana ("selalu ada nilai nol") malah akan berdampak membuat inisialisasi lebih kompleks daripada jika pelacakan inisialisasi ada dalam bahasa tersebut sejak hari pertama.

Ini adalah salah satu alasan teknis yang signifikan yang perlu ditangani. Bagi saya, alasan utamanya adalah mereka membuat perbaikan bertahap sangat tidak mungkin (lihat di atas). yaitu bagi saya pribadi, ini bukan pertanyaan tentang bagaimana menerapkannya, tetapi pada dasarnya saya menentang konsep tersebut.
Bagaimanapun, alasan mana yang "utama" sebenarnya adalah masalah selera dan preferensi.

Oke, saya mengerti ini. Kita hanya perlu juga melihat sudut pandang orang lain (saya tidak mengatakan Anda tidak melakukan itu, saya hanya menunjukkan :wink:) di mana mereka melihat ini sebagai sesuatu yang kuat untuk menulis program mereka. Apakah itu cocok dengan Go? Itu tergantung pada bagaimana ide dieksekusi dan diintegrasikan ke dalam bahasa, dan itulah yang kami semua coba lakukan di utas ini (saya kira)

Ini gagal jika Anda meneruskan tipe pointer. misalnya (...)

Saya tidak mengerti ini. Mengapa ini gagal? Anda baru saja memasukkan nilai ke dalam parameter fungsi, yang kebetulan berupa pointer dengan nilai nil . Kemudian Anda memodifikasi nilai itu di dalam fungsi. Diharapkan Anda tidak melihat efek tersebut di luar fungsi. Biarkan saya mengomentari beberapa contoh:

// Augmenting your example with more comments:
func FCurrentGo(p *T) {
    // Here "p" is just a value, which happens to be a pointer type. Doing...
    *p = 42
    // ...without checking first for "nil" is the recipe for hiding a bug that will crash the entire program, 
    // which is exactly what is happening in current Go code bases

    // The correct code would be:
    if p == nil {
        // panic or return error
    }
    *p = 42
}

func FWithDynamicZero(p *T) {
    // Here, again, p is just a value of a pointer type. Doing...
    *p = 42
    // would allocate a new T and assign 42. It is true that this doesn't have any effect on the "outside
    // world", which could be considered "incorrect" because you expected the function to do that.
    // If you really want to be sure "p" is pointing to something valid in the "outside world", then
    // check that:
    if p == nil {
        // panic or return error
    }
    *p = 42
}

func main() {
    var p *T
    FCurrentGo(p) // This will crash the program
        FWithDynamicZero(p) // This won't have any effect on "p". This is expected because "p" is not pointing
                            // to anything. No crash here.
    fmt.Println(p == nil) // It is true, as expected
}

Situasi serupa terjadi dengan metode penerima non-penunjuk, dan membingungkan bagi pendatang baru untuk Go (tetapi begitu Anda memahaminya, maka masuk akal):

type Point struct {
    x, y int
}

func (p Point) SetXY(x, y int) {
    p.x = x
    p.y = y
}

func main() {
    p := Point{x: 1, y: 2}
    p.SetXY(24, 42)

    pointerToP := &Point{x: 1, y: 2}
    pointerToP.SetXY(24, 42)

    fmt.Println(p, pointerToP) // Will print "{1 2} &{1 2}", which could confuse at first
}

Jadi kita harus memilih antara:

  • A) Kegagalan dengan crash
  • B) Kegagalan dengan non-modifikasi diam dari nilai yang ditunjukkan oleh pointer ketika pointer itu diteruskan ke suatu fungsi.

Perbaikan untuk kedua kasus sama: periksa nol sebelum melakukan apa pun. Tapi, bagi saya, A) jauh lebih berbahaya (seluruh aplikasi macet!).
B) dapat dianggap sebagai "kesalahan senyap", tetapi saya tidak akan menganggapnya sebagai kesalahan. Itu hanya terjadi ketika Anda meneruskan pointer ke fungsi dan seperti yang saya tunjukkan, ada kasus dengan struct yang berperilaku serupa. Ini tanpa mempertimbangkan manfaat besar yang dibawanya.

Catatan: Saya tidak mencoba membela ide "saya" secara membabi buta, saya benar-benar mencoba meningkatkan Go (yang sudah sangat bagus). Jika ada beberapa poin lain yang membuat ide tidak layak, maka saya tidak peduli untuk membuangnya dan terus berpikir ke arah lain.

Catatan 2: Akhirnya, ide ini hanya untuk nilai-nilai "nihil" dan tidak ada hubungannya dengan serikat pekerja yang didiskriminasi. Jadi saya akan membuat masalah yang berbeda untuk menghindari polusi yang satu ini

Oke, saya mengerti ini. Kita hanya perlu juga melihat sudut pandang orang lain (saya tidak mengatakan Anda tidak melakukan itu, saya hanya menyampaikan pendapat )

Pedang itu memotong dua arah. Anda mengatakan "alasan utama menahan ini adalah". Pernyataan itu menyiratkan bahwa kita semua setuju pada apakah kita ingin efek dari proposal ini. Saya pasti bisa setuju bahwa itu adalah detail teknis menahan saran khusus dibuat (atau setidaknya, bahwa saran harus mengatakan sesuatu tentang pertanyaan itu ). Tapi saya tidak suka diskusi dibingkai ulang secara diam-diam ke dunia paralel di mana kita mengasumsikan semua orang benar-benar menginginkannya .

Mengapa ini gagal?

Karena fungsi yang mengambil pointer akan, setidaknya sering, membuat janji untuk memodifikasi pointee. Jika fungsinya kemudian diam-diam tidak melakukan apa-apa, saya akan menganggap itu sebagai bug. Atau setidaknya, ini adalah argumen yang mudah dibuat, bahwa dengan mencegah nil-panic dengan cara ini, Anda memperkenalkan kelas bug baru.

Jika Anda memberikan penunjuk nol ke fungsi yang mengharapkan sesuatu di sana, itu adalah bug - dan saya tidak melihat nilai sebenarnya dalam membuat perangkat lunak buggy seperti itu terus berlanjut secara diam-diam. Saya dapat melihat nilai dalam ide asli untuk menangkap bug itu pada waktu kompilasi dengan memiliki dukungan untuk pointer yang tidak dapat dihapus, tetapi saya tidak melihat gunanya membiarkan bug itu tidak ditangkap sama sekali.

yaitu untuk berbicara, Anda menangani semacam masalah yang berbeda dari proposal sebenarnya dari pointer non-nilable: Untuk proposal itu, runtime-panic bukanlah masalahnya, tetapi hanya gejala - masalahnya adalah bug yang lewat secara tidak sengaja nil untuk sesuatu yang tidak diharapkan dan bug ini hanya tertangkap saat runtime.

Situasi serupa terjadi dengan metode penerima non-pointer

Saya tidak membeli analogi ini. IMO itu benar-benar masuk akal untuk dipertimbangkan

func Foo(p *int) { *p = 42 }

func main() {
    var v int
    Foo(&v)
    if v != 42 { panic("") }
}

menjadi kode yang benar. Saya tidak berpikir itu masuk akal untuk dipertimbangkan

func Foo(v int) { v = 42 }

func main( ){
    var v int
    Foo(v)
    if v != 42 { panic("") }
}

untuk menjadi benar. Mungkin jika Anda benar - setiap nilai adalah referensi (walaupun sejujurnya saya kesulitan menemukannya - bahkan Python dan Java hanya membuat sebagian besar referensi nilai). Tapi IMO, mengoptimalkan untuk kasus yang sia-sia, adalah wajar untuk menganggap bahwa orang memiliki beberapa keakraban dengan pointer vs nilai-nilai. Saya pikir bahkan pengembang Go yang berpengalaman akan melihat, katakanlah, metode dengan penerima penunjuk yang mengakses bidangnya sebagai benar, dan kode yang memanggil metode itu benar. Memang, itulah seluruh argumen untuk mencegah nil -pointers secara statis, bahwa terlalu mudah untuk secara tidak sengaja membuat pointer menjadi nihil dan kode yang terlihat benar gagal saat runtime.

Perbaikan untuk kedua kasus sama: periksa nol sebelum melakukan apa pun.

IMO perbaikan dalam semantik saat ini adalah tidak memeriksa nil dan menganggapnya sebagai bug jika seseorang melewati nil. Seperti, dalam contoh Anda, Anda menulis

// The correct code would be:
if p == nil {
    // panic or return error
}
*p = 42

Tapi saya tidak menganggap kode itu benar. nil -check tidak melakukan apa-apa, karena dereferensi nil sudah panik.

Tapi, bagi saya, A) jauh lebih berbahaya (seluruh aplikasi macet!).

Tidak apa-apa, tetapi perlu diingat bahwa banyak orang akan sangat tidak setuju dalam hal ini. Saya, secara pribadi, menganggap crash selalu lebih baik daripada melanjutkan dengan data yang rusak dan asumsi yang salah. Di dunia yang ideal, perangkat lunak saya tidak memiliki bug dan tidak pernah crash. Di dunia yang kurang ideal, program saya akan memiliki bug dan gagal dengan aman dengan mogok saat terdeteksi. Di dunia terburuk, program saya akan memiliki bug dan terus mendatangkan malapetaka ketika ditemui.

Pedang itu memotong dua arah. Anda mengatakan "alasan utama menahan ini adalah". Pernyataan itu menyiratkan bahwa kita semua setuju apakah kita menginginkan efek dari proposal ini. Saya pasti setuju bahwa itu adalah detail teknis yang menahan saran spesifik yang dibuat (atau setidaknya, saran apa pun harus mengatakan sesuatu tentang pertanyaan itu). Tapi saya tidak suka diskusi dibingkai ulang secara diam-diam ke dunia paralel di mana kita mengasumsikan semua orang benar-benar menginginkannya.

Yah, aku tidak ingin menyiratkan ini. Jika itu yang dipahami, maka saya mungkin tidak memilih kata yang tepat dan saya minta maaf. Saya hanya ingin memberikan beberapa ide untuk solusi yang mungkin, itu saja.

Saya menulis _"... sepertinya alasan utama yang menahan ini adalah...."_ berdasarkan kalimat Anda _"Ini adalah inti masalahnya"_ mengacu pada nilai nol. Itu sebabnya saya berasumsi bahwa nilai nol adalah hal utama yang menahan ini. Jadi itu adalah asumsi buruk saya.

Mengenai memperlakukan nil diam-diam vs memeriksanya pada waktu kompilasi: Saya setuju bahwa lebih baik memeriksanya pada waktu kompilasi. "Nilai nol dinamis" hanyalah iterasi pada saran asli ketika saya fokus untuk mengatasi masalah semua jenis-harus-memiliki-nilai-nol. Motivasi ekstra adalah bahwa saya _menganggapnya_ itu juga merupakan penghambat utama proposal serikat pekerja yang terdiskriminasi.
Jika kita hanya fokus pada masalah yang terkait dengan nil, saya lebih suka memeriksa tipe pointer non-nil pada waktu kompilasi.

Saya akan mengatakan bahwa pada titik tertentu, kami (dengan "kami" yang saya maksudkan ke seluruh komunitas Go) perlu menerima _semacam_ perubahan. Misalnya: Jika ada solusi yang baik untuk menghindari kesalahan nil sepenuhnya dan hal yang menahan ini adalah keputusan desain "semua jenis memiliki nilai nol dan terbuat dari 0", maka kita dapat mempertimbangkan idenya membuat beberapa penyesuaian atau perubahan pada keputusan itu jika itu membawa nilai.

Alasan utama saya mengatakan ini adalah kalimat Anda _"setiap jenis memiliki nilai nol, titik penuh "_. Saya biasanya tidak suka "menulis titik". Jangan salah paham! Saya sepenuhnya menerima bahwa Anda berpikir seperti itu, itu hanya cara berpikir saya: Saya lebih suka tidak ada dogma karena mereka dapat menyembunyikan jalan yang dapat mengarah pada solusi yang lebih baik.

Terakhir, mengenai hal ini:

Tidak apa-apa, tetapi perlu diingat bahwa banyak orang akan sangat tidak setuju dalam hal ini. Saya, secara pribadi, menganggap crash selalu lebih baik daripada melanjutkan dengan data yang rusak dan asumsi yang salah. Di dunia yang ideal, perangkat lunak saya tidak memiliki bug dan tidak pernah crash. Di dunia yang kurang ideal, program saya akan memiliki bug dan gagal dengan aman dengan mogok saat terdeteksi. Di dunia terburuk, program saya akan memiliki bug dan terus mendatangkan malapetaka ketika ditemui.

Saya sangat setuju dengan ini. Gagal dengan suara keras selalu lebih baik daripada gagal diam-diam. Namun, ada masalah di Go:

  • Jika Anda memiliki aplikasi dengan ribuan goroutine, kepanikan yang tidak tertangani di salah satunya membuat seluruh program macet. Ini berbeda dari bahasa lain, di mana hanya utas yang panik yang mogok

Mengesampingkan itu (walaupun cukup berbahaya), idenya adalah, kemudian, untuk menghindari seluruh kategori kegagalan ( nil -kegagalan terkait).

Jadi mari kita terus mengulangi ini dan mencoba mencari solusi.

Terima kasih atas waktu dan energi Anda!

Saya ingin melihat sintaks serikat terdiskriminasi karat daripada tipe jumlah haskell, ini memungkinkan penamaan varian dan memungkinkan proposal sintaks pencocokan pola yang lebih baik.
Implementasi dapat dilakukan seperti struct dengan bidang tag (tipe uint, tergantung pada jumlah varian) dan bidang gabungan (menyimpan data).
Fitur ini diperlukan untuk set varian tertutup (representasi status akan jauh lebih mudah dan bersih, dengan pemeriksaan waktu kompilasi). Menurut pertanyaan tentang antarmuka dan representasinya, saya pikir implementasinya dalam tipe sum tidak boleh lebih dari sekadar kasus tipe sum lainnya, karena antarmuka adalah tentang tipe apa pun yang sesuai dengan beberapa persyaratan, tetapi kasus penggunaan tipe sum berbeda.

Sintaksis:

type Type enum {
         Tuple (int,int),
         One int,
         None,
};

Dalam contoh di atas ukurannya adalah sizeof((int,int)).
Pencocokan pola dapat dilakukan dengan operator pencocokan yang baru dibuat, atau dalam operator sakelar yang ada, seperti:

var a Type
switch (a) {
         case Tuple{(b,c)}:
                    //do something
         case One{b}:
                    //do something else
         case None:
                    //...
}

Sintaks pembuatan:
var a Type = Type{One=12}
Perhatikan, bahwa dalam konstruksi instance enum hanya satu varian yang dapat ditentukan.

Nilai nol (masalah):
Kita dapat mengurutkan nama dalam urutan abjad, nilai nol enum akan menjadi nilai nol dari jenis anggota pertama dalam daftar anggota yang diurutkan.

PS Solusi masalah Nilai Nol sebagian besar ditentukan oleh kesepakatan.

Saya pikir menjaga nilai nol dari jumlah sebagai nilai nol dari bidang jumlah yang ditentukan pengguna pertama akan kurang membingungkan, mungkin

Saya pikir menjaga nilai nol dari jumlah sebagai nilai nol dari bidang jumlah yang ditentukan pengguna pertama akan kurang membingungkan, mungkin

Tetapi membuat nilai nol tergantung pada urutan deklarasi bidang, saya pikir itu lebih buruk.

Seseorang menulis desain doc?

Saya punya satu:
19412-discriminated_unions_and_pattern_matching.md.zip

Saya mengubah ini:

Saya pikir menjaga nilai nol dari jumlah sebagai nilai nol dari bidang jumlah yang ditentukan pengguna pertama akan kurang membingungkan, mungkin

Sekarang dalam perjanjian proposal saya tentang Nilai Nol (Masalah) pindah ke posisi urandom.

UPD: Dokumen desain diubah, perbaikan kecil.

Saya memiliki dua kasus penggunaan baru-baru ini, di mana saya membutuhkan tipe sum bawaan:

  1. Representasi pohon AST, seperti yang diharapkan. Awalnya menemukan perpustakaan yang merupakan solusi pada pandangan pertama, tetapi pendekatan mereka memiliki struct besar dengan banyak bidang nilable. Terburuk dari kedua dunia IMO. Tidak ada jenis keselamatan tentu saja. Menulis kita sendiri sebagai gantinya.
  2. Memiliki antrian tugas latar belakang yang telah ditentukan: kami memiliki layanan pencarian yang sedang dikembangkan sekarang dan operasi pencarian kami bisa menjadi terlalu panjang, dll. Jadi kami memutuskan untuk menjalankannya di latar belakang melalui pengiriman tugas operasi indeks pencarian ke saluran. Kemudian petugas operator akan memutuskan apa yang harus dilakukan dengan mereka lebih lanjut. Bisa menggunakan pola pengunjung, tapi ini jelas berlebihan untuk permintaan gRPC sederhana. Dan tidak terlalu jelas untuk mengatakan setidaknya, karena memperkenalkan ikatan antara petugas operator dan pengunjung.

Dalam kedua kasus menerapkan sesuatu seperti ini (pada contoh tugas ke-2):

type Task interface {
    task()
}

type SearchAdd struct {
    Ctx   context.Context
    ID    string
    Attrs Attributes
}

func (SearchAdd) task() {}

type SearchUpdate struct {
    Ctx         context.Context
    ID          string
    UpdateAttrs UpdateAttributes
}

func (SearchUpdate) task() {}

type SearchDelete struct {
    Ctx context.Context
    ID  string
}

func (SearchDelete) task() {}

Lalu

task := <- taskChannel

switch v := task.(type) {
case tasks.SearchAdd:
    resp, err := search.Add(task.Ctx, &search2.RequestAdd{…}
    if err != nil {
        log.Error().Err(err).Msg("blah-blah-blah")
    } else {
        if resp.GetCode() != search2.StatusCodeSuccess  {
            …
        } 
    }
case tasks.SearchUpdate:
    …
case tasks.SearchDelete:
    …
}

Ini hampir bagus. Hal buruknya Go tidak memberikan keamanan tipe penuh, yaitu tidak akan ada kesalahan kompilasi setelah tugas operasi indeks pencarian baru ditambahkan.

IMHO menggunakan tipe sum adalah solusi paling jelas untuk tugas semacam ini yang biasanya diselesaikan dengan pengunjung dan set operator, di mana fungsi pengunjung tidak banyak dan kecil dan pengunjung itu sendiri adalah tipe tetap.

Saya benar-benar percaya memiliki sesuatu seperti

type Task oneof {
    // SearchAdd holds a data for a new record in the search index
    SearchAdd {
        Ctx   context.Context
        ID    string
        Attrs Attributes   
    }

    // SearchUpdate update a record
    SearchUpdate struct {
        Ctx         context.Context
        ID          string
        UpdateAttrs UpdateAttributes
    }

    // SearchDelete delete a record
    SearchDelete struct {
        Ctx context.Context
        ID  string
    }
}

+

switch task {
case tasks.SearchAdd:
    // task is tasks.SearchAdd in this scope
case tasks.SearchUpdate:
case tasks.SearchDelete:
}

akan jauh lebih Goish dalam semangat daripada pendekatan lain yang memungkinkan Go dalam kondisinya saat ini. Tidak perlu pencocokan pola Haskellish, juist diving hingga tipe tertentu sudah lebih dari cukup.

Aduh, melewatkan inti dari proposal sintaksis. Memperbaikinya.

Dua versi, satu untuk tipe penjumlahan generik dan tipe penjumlahan untuk enumerasi:

Jenis penjumlahan umum

type Sum oneof {
    T₁ TypeDecl₁
    T₂ TypeDecl₂
    …
    Tₙ TypeDeclₙ
}

di mana T₁Tₙ adalah definisi tipe pada level yang sama dengan Sum ( oneof mengekspos mereka di luar cakupannya) dan Sum mendeklarasikan beberapa antarmuka yang hanya memenuhi T₁Tₙ .

Pemrosesan mirip dengan apa yang kita miliki (type) kecuali itu dilakukan secara implisit pada objek oneof dan harus ada pemeriksaan kompiler jika semua varian terdaftar.

Enumerasi aman tipe nyata

type Enum oneof {
    Value = iota
}

sangat mirip dengan iota dari consts, kecuali hanya nilai yang terdaftar secara eksplisit adalah Enum dan yang lainnya tidak.

switch task {
case tasks.SearchAdd:
    // task is tasks.SearchAdd in this scope
case tasks.SearchUpdate:
case tasks.SearchDelete:
}

akan jauh lebih Goish dalam semangat daripada pendekatan lain yang memungkinkan Go dalam kondisinya saat ini. Tidak perlu pencocokan pola Haskellish, juist diving hingga tipe tertentu sudah lebih dari cukup.

Saya tidak berpikir bahwa memanipulasi arti dari variabel task adalah ide yang baik, meskipun dapat diterima.

```go
switch task {
case tasks.SearchAdd:
    // task is tasks.SearchAdd in this scope
case tasks.SearchUpdate:
case tasks.SearchDelete:
}

akan jauh lebih Goish dalam semangat daripada pendekatan lain yang memungkinkan Go dalam kondisinya saat ini. Tidak perlu pencocokan pola Haskellish, juist diving hingga tipe tertentu sudah lebih dari cukup.

Saya tidak berpikir bahwa memanipulasi arti dari variabel tugas adalah ide yang baik, meskipun dapat diterima.
```

Good luck dengan pengunjung Anda kemudian.

@sirkon Apa maksudmu tentang pengunjung? Saya menyukai sintaks ini, namun haruskah sakelar ditulis seperti ini:

switch task {
case Task.SearchAdd:
    // task is Task.SearchAdd in this scope
case Task.SearchUpdate:
case Task.SearchDelete:
}

Juga apa yang tidak bernilai untuk Task menjadi? Sebagai contoh:

var task Task

Apakah itu nil ? Jika demikian, haruskah switch memiliki tambahan case nil ?
Atau apakah itu akan diinisialisasi ke tipe pertama? Ini akan menjadi canggung, karena urutan deklarasi tipe penting dengan cara yang tidak sebelumnya, namun mungkin akan baik-baik saja untuk enum numerik.

Saya berasumsi bahwa ini setara dengan switch task.(type) tetapi sakelar mengharuskan semua kasing ada di sana, bukan? seperti di .. jika Anda melewatkan satu kasus, kesalahan kompilasi. Dan default diperbolehkan. Apakah itu benar?

Apakah yang Anda maksud: pengunjung

Maksud saya mereka adalah satu-satunya opsi tipe aman di Go untuk fungsi semacam itu. Jauh lebih buruk dalam hal itu untuk serangkaian kasus tertentu (jumlah terbatas dari alternatif yang telah ditentukan sebelumnya).

Juga apa yang tidak bernilai untuk Task? Sebagai contoh:

var task Task

Saya khawatir itu harus menjadi tipe nilable di Go seperti ini

Atau apakah itu akan diinisialisasi ke tipe pertama?

akan terlalu aneh terutama untuk tujuan yang dimaksudkan.

Saya berasumsi bahwa ini setara dengan beralih tugas. (ketik) tetapi sakelar mengharuskan semua kasing ada di sana, bukan? seperti di .. jika Anda melewatkan satu kasus, kesalahan kompilasi.

Ya benar.

Dan tidak ada default yang diizinkan. Apakah itu benar?

Tidak, default diperbolehkan. Berkecil hati meskipun.

PS Sepertinya saya punya ide Go @ianlancetaylor dan orang Go lainnya punya tentang tipe jumlah. Sepertinya nilness membuat mereka cukup rentan terhadap NPD, karena Go tidak memiliki kendali atas nilai nil.

Jika nihil, maka saya kira tidak apa-apa. Saya lebih suka bahwa case nil adalah persyaratan untuk pernyataan switch. Melakukan if task != nil sebelumnya juga tidak apa-apa, saya hanya tidak terlalu menyukainya :|

Apakah ini akan diizinkan juga?

type Foo oneof {
  A = 3
  B = "3"
  C = 3.0
  D = struct { E bool }{ true }
}

Apakah ini akan diizinkan juga?

type Foo oneof {
  A = 3
  B = "3"
  C = 3.0
  D = struct { E bool }{ true }
}

Yah, tidak ada const, hanya

type Foo oneof {
    A <type reference>
}

atau

type Foo oneof {
    A = iota
    B
    C
}

atau

type Foo oneof {
    A = 1
    B = 2
    C = 3
}

Tidak ada kombinasi iotas dan nilai. Atau kombinasi dengan kontrol pada nilai, mereka tidak boleh diulang.

FWIW, satu hal yang menurut saya menarik tentang desain generik terbaru adalah bahwa ia menunjukkan tempat lain untuk menangani setidaknya beberapa kasus penggunaan tipe penjumlahan sambil menghindari jebakan tentang nilai nol. Ini mendefinisikan kontrak disjungtif, yang merupakan sum-ish dengan cara tertentu, tetapi karena mereka menggambarkan batasan dan bukan tipe, tidak perlu memiliki nilai nol (karena Anda tidak dapat mendeklarasikan variabel tipe itu). Artinya, paling tidak mungkin untuk menulis sebuah fungsi yang mengambil sekumpulan tipe yang mungkin terbatas, dengan pemeriksaan tipe waktu kompilasi dari set itu.

Sekarang, tentu saja, desain apa adanya tidak benar-benar berfungsi untuk kasus penggunaan yang dimaksudkan di sini: Disjungsi hanya mencantumkan jenis atau metode yang mendasarinya dan dengan demikian masih terbuka luas. Dan tentu saja, bahkan sebagai ide umum, itu sangat terbatas karena Anda tidak dapat membuat instance fungsi atau nilai generik (atau pengambilan jumlah). Tetapi IMO itu menunjukkan bahwa ruang desain untuk menangani beberapa kasus penggunaan jumlah jauh lebih besar daripada gagasan jenis jumlah itu sendiri. Dan pemikiran tentang jumlah itu lebih terpaku pada solusi spesifik, bukan masalah spesifik.

Bagaimanapun. Hanya berpikir itu menarik.

@Merovius membuat poin bagus tentang desain generik terbaru yang mampu menangani beberapa kasus penggunaan tipe jumlah. Misalnya, fungsi ini yang digunakan sebelumnya di utas:

func addOne(x int|float64) int|float64 {
    switch x := x.(type) {
    case int:
        return x + 1
    case float64:
         return x + 1
    }
}

akan menjadi:

contract intOrFloat64(T) {
    T int, float64
}

func addOne(type T intOrFloat64) (x T) T {
    return x + 1
}

Sejauh menyangkut jenis jumlah itu sendiri, jika obat generik akhirnya mendarat, saya akan lebih meragukan daripada sekarang tentang apakah manfaat memperkenalkannya akan lebih besar daripada biaya untuk bahasa sederhana seperti Go.

Namun, jika sesuatu harus dilakukan, maka solusi IMO yang paling sederhana dan paling tidak mengganggu adalah gagasan @ianlancetaylor tentang 'antarmuka terbatas' yang akan diimplementasikan dengan cara yang persis sama dengan antarmuka 'tidak terbatas' saat ini tetapi hanya dapat dipenuhi oleh jenis yang ditentukan. Faktanya, jika Anda mengambil lembaran dari buku desain generik, dan membuat batasan tipe pada baris pertama dari blok antarmuka:

type intOrFloat64 interface{ type int, float64 }    

maka ini akan sepenuhnya kompatibel karena Anda tidak memerlukan kata kunci baru (seperti restrict ) sama sekali. Anda masih dapat menambahkan metode ke antarmuka dan ini akan menjadi kesalahan waktu kompilasi jika metode tidak didukung oleh semua jenis yang ditentukan.

Saya tidak melihat masalah sama sekali dalam menetapkan nilai ke variabel tipe antarmuka terbatas. Jika tipe nilai pada RHS (atau tipe default dari literal yang tidak diketik) tidak sama persis dengan salah satu tipe yang ditentukan, maka itu tidak akan dikompilasi. Jadi kita akan memiliki:

var v1 intOrFloat64 = 1        // compiles, dynamic type int
var v2 intOrFloat64 = 1.0      // compiles, dynamic type float64
var v3 intOrFloat64 = 1 + 2i   // doesn't compile, complex128 is not a specified type

Ini akan menjadi kesalahan waktu kompilasi untuk kasus sakelar tipe yang tidak cocok dengan tipe yang ditentukan dan pemeriksaan kelengkapan dapat diterapkan. Namun, pernyataan tipe masih diperlukan untuk mengonversi nilai antarmuka terbatas ke nilai tipe dinamisnya seperti sekarang ini.

Nilai nol tidak menjadi masalah dengan pendekatan ini (atau setidaknya tidak lebih menjadi masalah daripada saat ini dengan antarmuka pada umumnya). Nilai nol dari antarmuka terbatas adalah nil (menyiratkan bahwa saat ini tidak berisi apa pun) dan tipe yang ditentukan tentu saja memiliki nilai nolnya sendiri, secara internal, yang akan menjadi nil untuk tipe nilable.

Semua ini tampaknya sangat bisa diterapkan bagi saya, seperti yang saya katakan sebelumnya, adalah keamanan waktu kompilasi yang diperoleh benar-benar sepadan dengan kompleksitas tambahan - saya ragu karena saya tidak pernah benar-benar merasakan kebutuhan untuk tipe jumlah dalam pemrograman saya sendiri.

IIUC hal generik tidak akan menjadi tipe dinamis, jadi keseluruhan poin ini tidak berlaku. Namun jika antarmuka diizinkan untuk berfungsi sebagai kontrak (yang saya ragu) itu tidak akan menyelesaikan pemeriksaan dan enum yang lengkap yang merupakan (saya pikir, mungkin tidak?) tentang sumtypes.

@alanfo , @Merovius Terima kasih atas isyaratnya; menarik bahwa diskusi ini berbalik ke arah ini:

Saya suka membalikkan sudut pandang hanya dalam sepersekian detik: Saya mencoba memahami mengapa kontrak tidak dapat diganti sepenuhnya dengan antarmuka berparameter yang mengizinkan pembatasan jenis yang disebutkan di atas. Saat ini saya tidak melihat alasan teknis yang kuat, kecuali bahwa jenis antarmuka "jumlah" seperti itu, ketika digunakan sebagai jenis "jumlah" ingin membatasi kemungkinan nilai dinamis ke jenis yang disebutkan dalam antarmuka, sementara - jika antarmuka yang sama digunakan dalam posisi kontrak - tipe yang disebutkan dalam antarmuka perlu berfungsi sebagai tipe dasar untuk menjadi batasan umum yang cukup berguna.

@Goodwine
Saya tidak menyarankan bahwa desain generik akan membahas semua yang mungkin ingin dilakukan seseorang dengan tipe jumlah - seperti yang dijelaskan oleh @Merovius dengan jelas dalam posting terakhirnya, mereka tidak akan melakukannya. Secara khusus, batasan tipe yang diusulkan untuk obat generik hanya mencakup tipe bawaan dan semua tipe turunan darinya. Dari sudut pandang tipe penjumlahan, yang pertama terlalu sempit dan yang terakhir terlalu lebar.

Namun, desain generik akan memungkinkan seseorang untuk menulis fungsi yang beroperasi pada sekumpulan tipe terbatas yang akan ditegakkan oleh kompiler dan ini adalah sesuatu yang tidak dapat kita lakukan sama sekali saat ini.

Sejauh menyangkut antarmuka terbatas, kompiler akan mengetahui tipe yang tepat yang dapat digunakan dan oleh karena itu menjadi layak untuk melakukan pemeriksaan lengkap dalam pernyataan sakelar tipe.

@Griesemer

Saya bingung dengan apa yang Anda katakan karena saya pikir draf dokumen desain generik menjelaskan dengan cukup jelas (di bagian "Mengapa tidak menggunakan antarmuka alih-alih kontrak") mengapa yang terakhir dianggap sebagai kendaraan yang lebih baik daripada yang pertama untuk mengekspresikan batasan generik.

Secara khusus, kontrak dapat mengekspresikan hubungan antara parameter tipe dan oleh karena itu hanya diperlukan satu kontrak. Setiap parameter tipenya dapat digunakan sebagai tipe penerima dari metode yang tercantum dalam kontrak.

Hal yang sama tidak dapat dikatakan tentang antarmuka, berparameter atau tidak. Jika mereka memiliki kendala sama sekali, setiap parameter tipe akan membutuhkan antarmuka yang terpisah.

Ini membuatnya lebih canggung untuk mengekspresikan hubungan antara parameter tipe menggunakan antarmuka meskipun bukan tidak mungkin seperti yang ditunjukkan oleh contoh grafik.

Namun, jika Anda berpikir bahwa kami dapat "membunuh dua burung dengan satu batu" dengan menambahkan batasan tipe ke antarmuka dan kemudian menggunakannya untuk tujuan tipe generik dan jumlah, maka (terlepas dari masalah yang Anda sebutkan) saya pikir Anda mungkin benar bahwa ini akan layak secara teknis.

Saya kira tidak masalah jika batasan tipe antarmuka dapat mencakup tipe 'non built-in' sejauh menyangkut obat generik meskipun beberapa cara perlu ditemukan untuk membatasi mereka ke tipe yang tepat (dan bukan tipe turunan juga) jadi mereka akan cocok untuk tipe sum. Mungkin kita bisa menggunakan const type untuk yang terakhir (atau bahkan hanya const ) jika kita ingin tetap menggunakan kata kunci saat ini.

@griesemer Ada beberapa alasan mengapa tipe antarmuka

  1. Parameter tipe sama dengan tipe parameter lainnya.
    Dalam tipe seperti

    type C2(type T C1) interface { ... }
    

    parameter tipe T ada di luar antarmuka itu sendiri. Argumen tipe apa pun yang diteruskan sebagai T harus sudah diketahui untuk memenuhi kontrak C1 , dan isi antarmuka tidak dapat membatasi lebih lanjut T . Ini berbeda dengan parameter kontrak, yang dibatasi oleh isi kontrak sebagai akibat dari yang dimasukkan ke dalamnya. Ini berarti bahwa setiap parameter tipe ke suatu fungsi harus dibatasi secara independen sebelum diteruskan sebagai parameter ke batasan pada parameter tipe lainnya.

  2. Tidak ada cara untuk memberi nama jenis penerima di badan antarmuka.
    Antarmuka harus membiarkan Anda menulis sesuatu seperti:

    type C3(type U C1) interface(T) {
        Add(T) T
    }
    

    di mana T menunjukkan tipe penerima.

  3. Beberapa tipe antarmuka tidak akan memuaskan diri mereka sendiri sebagai batasan umum.
    Setiap operasi yang mengandalkan beberapa nilai dari jenis penerima tidak kompatibel dengan pengiriman dinamis. Oleh karena itu, operasi ini tidak dapat digunakan pada nilai antarmuka. Ini berarti bahwa antarmuka tidak akan memuaskan dirinya sendiri (misalnya sebagai argumen tipe ke parameter tipe yang dibatasi oleh antarmuka yang sama). Ini akan mengejutkan. Salah satu solusinya adalah tidak mengizinkan pembuatan nilai antarmuka untuk antarmuka seperti itu sama sekali, tetapi ini akan melarang kasus penggunaan yang dibayangkan di sini.

Adapun untuk membedakan antara batasan tipe yang mendasari dan batasan identitas tipe, ada satu metode yang mungkin berhasil. Bayangkan kita bisa mendefinisikan batasan khusus, seperti

contract (T) indenticalTo(U) {
    *T *U
}

(Di sini, saya menggunakan notasi yang diciptakan untuk menentukan satu jenis sebagai "penerima". Saya akan mengucapkan kontrak dengan jenis penerima eksplisit sebagai "batasan", seperti fungsi dengan penerima diucapkan "metode". Parameter setelah nama kontrak adalah parameter tipe normal dan tidak dapat muncul di sisi kiri klausa kendala di badan kendala.)

Karena tipe dasar dari tipe pointer literal itu sendiri, batasan ini menyiratkan bahwa T identik dengan U . Karena ini dideklarasikan sebagai kendala, Anda dapat menulis (identicalTo(int)), (identicalTo(uint)), ... sebagai disjungsi kendala.

Sementara kontrak mungkin berguna untuk mengekspresikan beberapa jenis jumlah, saya tidak berpikir Anda dapat mengekspresikan jenis jumlah umum dengan mereka. Dari apa yang saya lihat dari draf, kita harus membuat daftar tipe konkret, jadi Anda tidak dapat menulis sesuatu seperti ini:

contract Foo(T, U) {
    T U, int64
}

Yang mana yang perlu mengekspresikan tipe penjumlahan generik dari tipe yang tidak diketahui dan satu/lebih tipe yang diketahui. Bahkan jika desainnya memungkinkan untuk konstruksi seperti itu, mereka akan terlihat aneh saat digunakan, karena kedua parameter secara efektif akan menjadi hal yang sama.

Saya telah memikirkan lebih jauh tentang bagaimana rancangan desain generik dapat berubah jika antarmuka diperluas untuk menyertakan batasan tipe dan kemudian digunakan untuk menggantikan kontrak dalam desain.

Mungkin paling mudah untuk menganalisis situasi jika kita mempertimbangkan jumlah parameter tipe yang berbeda:

Tidak ada parameter

Tidak ada perubahan :)

Satu parameter

Tidak ada masalah nyata di sini. Antarmuka berparameter (sebagai lawan dari yang non-generik) hanya akan diperlukan jika parameter tipe merujuk ke dirinya sendiri dan/atau beberapa tipe tetap independen lainnya diperlukan untuk membuat instance antarmuka.

Dua atau lebih parameter

Seperti yang disebutkan sebelumnya, setiap parameter tipe perlu dibatasi secara individual jika itu membutuhkan batasan sama sekali.

Antarmuka berparameter hanya diperlukan jika:

  1. Parameter tipe mengacu pada dirinya sendiri.

  2. Antarmuka merujuk ke parameter tipe lain atau parameter yang _sudah dideklarasikan_ di bagian parameter tipe (mungkin kita tidak ingin mundur di sini).

  3. Beberapa tipe tetap independen lainnya diperlukan untuk membuat instance antarmuka.

Dari ini (2) benar-benar satu-satunya kasus yang merepotkan karena akan mengesampingkan parameter tipe yang merujuk satu sama lain seperti pada contoh grafik. Baik yang pertama mendeklarasikan 'Node' atau 'Edge', antarmuka pembatasnya masih memerlukan yang lain untuk diteruskan sebagai parameter tipe.

Namun, seperti yang ditunjukkan dalam dokumen desain, Anda dapat mengatasinya dengan mendeklarasikan NodeInterface dan EdgeInterface non-parameter (karena mereka tidak merujuk pada diri mereka sendiri) di tingkat atas karena tidak akan ada masalah dalam merujuk satu sama lain apa pun alasannya. perintah deklarasi. Anda kemudian dapat menggunakan antarmuka ini untuk membatasi parameter tipe struct Graph dan metode 'Baru' yang terkait.

Jadi sepertinya tidak ada masalah yang tidak dapat diatasi di sini bahkan jika ide kontraknya lebih bagus.

Agaknya, comparable sekarang bisa menjadi antarmuka bawaan daripada kontrak.

Antarmuka tentu saja dapat disematkan satu sama lain seperti yang sudah mereka lakukan.

Saya tidak yakin bagaimana seseorang akan menangani masalah metode penunjuk (dalam kasus di mana ini perlu ditentukan dalam kontrak) karena Anda tidak dapat menentukan penerima untuk metode antarmuka. Mungkin beberapa sintaks khusus (seperti mendahului nama metode dengan tanda bintang) akan diperlukan untuk menunjukkan metode pointer.

Beralih sekarang ke pengamatan @stevenblenkinsop , saya bertanya-tanya apakah itu akan membuat hidup lebih mudah jika antarmuka

Secara pribadi, saya tidak menganggapnya mengejutkan bahwa beberapa tipe antarmuka tidak akan dapat memuaskan diri mereka sendiri sebagai batasan umum. Jenis antarmuka bukanlah jenis penerima yang valid dalam hal apa pun dan karenanya tidak dapat memiliki metode.

Meskipun gagasan Steven tentang fungsi bawaan identikTo() akan berfungsi, bagi saya tampaknya berpotensi bertele-tele untuk menentukan jenis jumlah. Saya lebih suka sintaks yang memungkinkan seseorang untuk menentukan seluruh baris tipe secara tepat.

@urandom benar, tentu saja, bahwa karena draf generik saat ini berdiri, seseorang hanya dapat mencantumkan tipe beton ( bawaan ). Namun, ini jelas harus berubah jika antarmuka terbatas digunakan sebagai gantinya untuk generik dan tipe sum. Jadi saya tidak akan mengesampingkan bahwa sesuatu seperti ini akan diizinkan di lingkungan terpadu:

interface Foo(T) {
    const type T, int64  // 'const' indicates types are exact i.e. no derived types
}

mengapa kita tidak bisa menambahkan Serikat Terdiskriminasi saja ke dalam bahasa daripada menciptakan jalan-jalan lain tentang ketidakhadiran mereka?

@griesemer Anda mungkin atau mungkin tidak menyadarinya, tetapi saya lebih suka menggunakan antarmuka untuk menentukan batasan sejak awal :) Saya tidak lagi berpikir ide persis yang saya kemukakan di pos itu adalah cara yang harus dilakukan (terutama hal-hal Saya sarankan untuk mengatasi operator). Dan saya sangat menyukai iterasi terbaru dari desain kontrak lebih dari yang sebelumnya. Tetapi secara umum, saya sepenuhnya setuju bahwa (mungkin diperpanjang) antarmuka sebagai kendala layak dan layak dipertimbangkan.

@urandom

Saya tidak berpikir Anda dapat mengekspresikan tipe penjumlahan umum dengan mereka

Saya ingin mengulangi bahwa poin saya bukan "Anda dapat membuat tipe penjumlahan dengan mereka", tetapi "Anda dapat menyelesaikan beberapa masalah yang diselesaikan oleh tipe penjumlahan". Jika pernyataan masalah Anda adalah "Saya ingin tipe penjumlahan", maka tidak mengherankan bahwa tipe penjumlahan adalah satu-satunya solusi. Saya hanya ingin mengungkapkan bahwa mungkin untuk melakukannya tanpa mereka, jika kita fokus pada masalah yang ingin Anda selesaikan dengan mereka.

@alanfo

Ini membuatnya lebih canggung untuk mengekspresikan hubungan antara parameter tipe menggunakan antarmuka meskipun bukan tidak mungkin seperti yang ditunjukkan oleh contoh grafik.

Saya pikir "canggung" itu subjektif. Secara pribadi, saya menemukan menggunakan antarmuka berparameter lebih alami dan contoh grafik ilustrasi yang sangat bagus. Bagi saya, Graph adalah entitas, bukan hubungan antara sejenis Edge dan semacam Node.js.

Tapi TBH, saya tidak berpikir salah satu dari mereka benar-benar lebih atau kurang canggung - Anda menulis kode yang hampir sama persis untuk mengekspresikan hal yang hampir sama persis. Dan FWIW, ada seni sebelumnya untuk ini. Kelas tipe Haskell berperilaku sangat mirip dengan antarmuka dan seperti yang ditunjukkan oleh artikel wiki itu, menggunakan kelas tipe multi-parameter untuk mengekspresikan hubungan antar tipe adalah hal yang cukup normal untuk dilakukan.

@stevenblenkinsop

Tidak ada cara untuk memberi nama jenis penerima di badan antarmuka.

Cara Anda mengatasinya adalah dengan type-arguments di situs-penggunaan. yaitu

type Adder(type T) interface {
    Add(t T) T
}

func Sum(type T Adder(T)) (vs []T) T {
    var t T
    for _, v := range vs {
        t = t.Add(v)
    }
    return t
}

Ini membutuhkan perhatian tentang cara kerja penyatuan, sehingga Anda dapat mengizinkan parameter tipe referensi sendiri, tetapi saya pikir itu dapat dibuat berfungsi.

Anda 1. dan 3. Saya tidak begitu mengerti, harus saya akui. Saya akan mendapat manfaat dari beberapa contoh konkret.


Bagaimanapun, agak tidak jujur ​​untuk menghentikan ini di akhir melanjutkan diskusi ini, tetapi ini mungkin bukan masalah yang tepat untuk membicarakan hal-hal kecil dari desain generik. Saya hanya mengangkatnya untuk memperluas ruang desain untuk masalah ini sedikit :) Karena rasanya sudah lama sejak ide-ide baru dibawa ke dalam diskusi seputar tipe sum.

```go
switch task {
case tasks.SearchAdd:
    // task is tasks.SearchAdd in this scope
case tasks.SearchUpdate:
case tasks.SearchDelete:
}

akan jauh lebih Goish dalam semangat daripada pendekatan lain yang memungkinkan Go dalam kondisinya saat ini. Tidak perlu pencocokan pola Haskellish, juist diving hingga tipe tertentu sudah lebih dari cukup.
Saya tidak berpikir bahwa memanipulasi arti dari variabel tugas adalah ide yang baik, meskipun dapat diterima.

Good luck dengan pengunjung Anda kemudian.

Mengapa menurut Anda pencocokan pola tidak dapat dilakukan di Go? Jika Anda kekurangan contoh pencocokan patten, lihat, misalnya, Rust.

@Merovius re: "Bagi saya, Grafik adalah entitas"

Apakah itu entitas waktu kompilasi atau apakah ia memiliki representasi saat runtime? Salah satu perbedaan utama antara kontrak dan antarmuka adalah bahwa antarmuka adalah objek runtime. Ini berpartisipasi dalam pengumpulan sampah, memiliki pointer ke objek runtime lainnya, dan seterusnya. Mengonversi dari kontrak ke antarmuka berarti memperkenalkan objek runtime sementara baru yang memiliki pointer ke node/vertex yang dikandungnya (berapa banyak?), yang tampak canggung ketika Anda memiliki kumpulan fungsi grafik, yang masing-masing mungkin lebih alami mengambil parameter yang menunjuk ke berbagai bagian grafik dengan caranya sendiri, tergantung pada kebutuhan fungsi.

Intuisi Anda mungkin disesatkan dengan menggunakan "Grafik" untuk sebuah kontrak, karena "Grafik" tampak seperti objek dan kontrak tersebut tidak benar-benar menentukan subgraf tertentu; ini lebih seperti mendefinisikan serangkaian istilah untuk digunakan nanti, seperti yang Anda lakukan dalam matematika atau hukum. Dalam beberapa kasus, Anda mungkin menginginkan kontrak grafik dan antarmuka grafik, yang mengakibatkan bentrokan nama yang mengganggu. Saya tidak bisa memikirkan nama yang lebih baik dari atas kepala saya, meskipun.

Sebaliknya, serikat terdiskriminasi adalah objek runtime. Meskipun tidak membatasi implementasi, Anda perlu memikirkan seperti apa array dari mereka. Array N-item membutuhkan diskriminator N dan nilai N, dan ada berbagai cara yang mungkin dilakukan. (Julia memiliki representasi yang menarik, terkadang menempatkan diskriminator dan nilai dalam susunan terpisah.)

Untuk menyarankan pengurangan kesalahan yang saat ini terjadi di semua tempat dengan skema interface{} , tetapi untuk menghapus pengetikan berkelanjutan dari operator | , saya akan menyarankan yang berikut:

type foobar union {
    int
    float64
}

Hanya kasus penggunaan saja mengganti banyak interface{} dengan jenis keamanan jenis ini akan menjadi keuntungan besar bagi perpustakaan. Hanya dengan melihat setengah dari hal-hal di perpustakaan crypto bisa menggunakan ini.

Masalah seperti: ah Anda memberikan ecdsa.PrivateKey alih-alih *ecdsa.PrivateKey - inilah kesalahan umum yang hanya didukung oleh ecdsa.PrivateKey. Fakta sederhana bahwa ini harus menjadi tipe serikat pekerja yang jelas akan sedikit meningkatkan keamanan tipe.

Meskipun saran ini membutuhkan lebih banyak _space_ dibandingkan dengan int|float64 , saran ini memaksa pengguna untuk memikirkan hal ini. Menjaga basis kode jauh lebih bersih.

Untuk menyarankan pengurangan kesalahan yang saat ini terjadi di semua tempat dengan skema interface{} , tetapi untuk menghapus pengetikan berkelanjutan dari operator | , saya akan menyarankan yang berikut:

type foobar union {
    int
    float64
}

Hanya kasus penggunaan saja mengganti banyak interface{} dengan jenis keamanan jenis ini akan menjadi keuntungan besar bagi perpustakaan. Hanya dengan melihat setengah dari hal-hal di perpustakaan crypto bisa menggunakan ini.

Masalah seperti: ah Anda memberikan ecdsa.PrivateKey alih-alih *ecdsa.PrivateKey - inilah kesalahan umum yang hanya didukung oleh ecdsa.PrivateKey. Fakta sederhana bahwa ini harus menjadi tipe serikat pekerja yang jelas akan sedikit meningkatkan keamanan tipe.

Meskipun saran ini membutuhkan lebih banyak _space_ dibandingkan dengan int|float64 , saran ini memaksa pengguna untuk memikirkan hal ini. Menjaga basis kode jauh lebih bersih.

Lihat ini (komentar) , ini proposal saya.

Sebenarnya kita bisa memperkenalkan kedua ide kita ke dalam bahasa. Ini akan mengarah pada keberadaan dua cara asli untuk melakukan ADT, tetapi dengan sintaks yang berbeda.

Proposal saya untuk fitur, terutama pencocokan pola, Anda untuk kompatibilitas dan kemampuan untuk memanfaatkan fitur untuk basis kode lama.

Tapi sepertinya berlebihan, bukan?

Juga, tipe jumlah dapat dibuat untuk memiliki nil sebagai nilai defaultnya. Tentu saja itu akan membutuhkan kasus nil di setiap sakelar.
Pencocokan Pola dapat dilakukan seperti:
-- deklarasi

type U enum{
    A(int64),
    B(string),
}

-- cocok

...
var a U
...
switch a {
    case A{b}:
         //process b here
    case B{b}:
         //...
    case nil:
         //...
}
...

Jika seseorang tidak menyukai pencocokan pola - lihat proposal sirkon di atas.

Juga, tipe jumlah dapat dibuat untuk memiliki nil sebagai nilai defaultnya. Tentu saja itu akan membutuhkan kasus nil di setiap sakelar.

Bukankah lebih mudah untuk melarang nilai yang tidak dimulai pada waktu kompilasi? Untuk kasus di mana kita membutuhkan nilai yang diinisialisasi, kita dapat menambahkannya ke tipe jumlah: yaitu

type U enum {
  None
  A(string)
  B(uint64)
}
...
var a U.None
...
switch a {
  case U.None: ...
  case U.A(str): ...
  case U.B(i): ...
}

Juga, tipe jumlah dapat dibuat untuk memiliki nil sebagai nilai defaultnya. Tentu saja itu akan membutuhkan kasus nil di setiap sakelar.

Bukankah lebih mudah untuk melarang nilai yang tidak dimulai pada waktu kompilasi? Untuk kasus di mana kita membutuhkan nilai yang diinisialisasi, kita dapat menambahkannya ke tipe jumlah: yaitu

Memecah kode yang ada.

Juga, tipe jumlah dapat dibuat untuk memiliki nil sebagai nilai defaultnya. Tentu saja itu akan membutuhkan kasus nil di setiap sakelar.

Bukankah lebih mudah untuk melarang nilai yang tidak dimulai pada waktu kompilasi? Untuk kasus di mana kita membutuhkan nilai yang diinisialisasi, kita dapat menambahkannya ke tipe jumlah: yaitu

Memecah kode yang ada.

Tidak ada kode yang ada dengan tipe jumlah. Meskipun saya pikir nilai default harus menjadi sesuatu yang didefinisikan dalam tipe itu sendiri. Entah entri pertama, atau abjad pertama, atau sesuatu.

Tidak ada kode yang ada dengan tipe jumlah. Meskipun saya pikir nilai default harus menjadi sesuatu yang didefinisikan dalam tipe itu sendiri. Entah entri pertama, atau abjad pertama, atau sesuatu.

Saya setuju dengan Anda pada pemikiran pertama, tetapi setelah beberapa refleksi, nama cadangan baru untuk serikat dapat digunakan sebelumnya di beberapa basis kode (union, enum, dll.)

Saya pikir kewajiban untuk memeriksa nol akan sangat menyakitkan untuk digunakan.

Sepertinya perubahan besar untuk kompatibilitas mundur yang hanya bisa diselesaikan oleh Go2.0

Tidak ada kode yang ada dengan tipe jumlah. Meskipun saya pikir nilai default harus menjadi sesuatu yang didefinisikan dalam tipe itu sendiri. Entah entri pertama, atau abjad pertama, atau sesuatu.

Tetapi ada banyak kode go yang ada yang memiliki segalanya nihil. Itu pasti akan merusak perubahan. Lebih buruk lagi, gofix dan alat serupa hanya dapat mengubah tipe variabel menjadi Opsi (dari tipe yang sama) menghasilkan setidaknya kode jelek, semua kasus lain itu hanya akan merusak segalanya di dunia.

Jika tidak ada yang lain, reflect.Zero perlu mengembalikan sesuatu . Tetapi semua ini adalah rintangan teknis yang dapat diselesaikan - misalnya, rintangan ini cukup jelas jika nilai nol dari tipe-jumlah didefinisikan dengan baik dan mungkin akan "panik", jika tidak. Pertanyaan yang lebih besar masih mengapa pilihan tertentu adalah pilihan yang benar dan apakah dan bagaimana pilihan cocok dengan bahasa pada umumnya. IMO, cara terbaik untuk mengatasi ini masih berbicara tentang kasus-kasus konkret di mana jenis jumlah mengatasi masalah tertentu atau kekurangannya. Tiga kriteria untuk laporan pengalaman berlaku untuk itu.

Perhatikan khususnya, bahwa "tidak boleh ada nilai nol dan tidak boleh membuat nilai yang tidak diinisialisasi" dan "default harus menjadi entri pertama" telah disebutkan di atas, beberapa kali. Jadi apakah menurut Anda seharusnya seperti ini atau itu tidak benar-benar menambah informasi baru. Tapi itu membuat utas yang sudah sangat besar menjadi lebih lama dan lebih sulit di masa depan untuk menemukan informasi yang relevan di dalamnya.

Mari kita pertimbangkan mencerminkan. Baik. Ada Jenis Tidak Valid, yang memiliki nilai int default 0. Jika Anda memiliki fungsi yang menerima reflect.Kind, dan Anda melewati variabel yang tidak diinisialisasi dari jenis itu, itu akan berakhir menjadi Tidak Valid. Jika, reflect.Kind secara hipotetis dapat diubah menjadi tipe jumlah, mungkin harus mempertahankan perilaku memiliki entri yang tidak valid bernama sebagai entri default, daripada mengandalkan nilai nil.

Sekarang, mari kita pertimbangkan html/template.contentType. Tipe Plain adalah nilai defaultnya, dan memang diperlakukan seperti itu oleh fungsi stringify, karena ini adalah fallback. Di masa depan jumlah hipotetis, Anda tidak hanya masih membutuhkan perilaku itu, tetapi juga tidak mungkin menggunakan nilai nil untuk itu, karena nil tidak akan berarti apa-apa bagi pengguna jenis ini. Akan sangat wajib untuk selalu mengembalikan nilai bernama di sini, dan Anda memiliki default yang jelas tentang nilai yang seharusnya.

Ini saya lagi dengan contoh lain di mana tipe data aljabar/variadik/jumlah/apa pun berfungsi dengan baik.

Jadi, kami menggunakan database noSQL tanpa transaksi (sistem terdistribusi, transaksi tidak bekerja untuk kami) namun kami menyukai integritas dan konsistensi data untuk alasan yang jelas dan harus mengatasi masalah akses bersamaan, biasanya dengan kueri pembaruan bersyarat yang sedikit rumit pada satu record (penulisan record tunggal bersifat atomik).

Saya memiliki tugas baru untuk menulis satu set entitas apa yang dapat dimasukkan, ditambahkan, atau dihapus (hanya satu dari operasi ini).

Jika kita bisa memiliki sesuatu seperti

type EntityOp oneof {
    Insert   Reference
    NewState string
    Delete   struct{}
}

Metodenya bisa saja

type DB interface {
    …
    Capture(ctx context.Context, processID string, ops map[string]EntityOp) (bool, error)
}

Salah satu penggunaan fantastis untuk jumlah waktu adalah untuk mewakili node dalam AST. Yang lain adalah mengganti nil dengan option yang diperiksa pada waktu kompilasi.

@DemiMarie tapi di Go hari ini, jumlah ini juga bisa nihil, seperti yang saya usulkan di atas, kita bisa membuat nil menjadi varian dari setiap enum, akan ada kasus nihil di setiap sakelar tetapi kewajiban ini tidak terlalu buruk, terutama jika kita menginginkan fitur ini tanpa merusak semua kode go yang ada (saat ini kami memiliki semuanya tanpa nilai)

Tidak tahu apakah itu termasuk di sini, tetapi semua ini tetap saya TypeScript, di mana fitur yang sangat keren yang disebut "Jenis Literal String" ada dan kita dapat melakukannya:

var name: "Peter" | "Consuela"; // string type with compile-time constraint

Ini seperti string enum, yang menurut saya jauh lebih baik daripada enum numerik tradisional.

@Merovius
contoh konkret bekerja dengan JSON sewenang-wenang.
Dalam Rust itu dapat direpresentasikan sebagai
nilai enum {
Batal,
Bool (bool),
Nomor (Nomor),
Tali (Tali),
Array(Vec),
Obyek (Peta),
}

Jenis serikat pekerja sebagai dua keuntungan:

  1. Mendokumentasikan kode sendiri
  2. Mengizinkan kompiler atau go vet untuk memeriksa penggunaan yang salah dari jenis gabungan
    (misalnya sakelar di mana tidak semua jenis dicentang)

Untuk sintaks, berikut ini harus kompatibel dengan Go1 , seperti dengan type alias :

type Token = int | float64 | string

Jenis serikat pekerja dapat diimplementasikan secara internal sebagai antarmuka; yang penting adalah menggunakan tipe gabungan memungkinkan kode lebih mudah dibaca dan menangkap kesalahan seperti

var tok Token

switch t := tok.(type) {
case int:
    // do something
}

Kompiler seharusnya memunculkan kesalahan, karena tidak semua tipe Token digunakan dalam sakelar.

Masalah dengan ini, adalah bahwa (sepengetahuan saya) tidak ada cara untuk menyimpan tipe pointer (atau tipe yang berisi pointer, seperti string ) dan tipe non-pointer bersama-sama. Bahkan tipe dengan tata letak yang berbeda tidak akan berfungsi. Jangan ragu untuk mengoreksi saya tetapi masalahnya adalah GC yang tepat tidak berfungsi dengan baik dengan variabel yang dapat menjadi pointer dan variabel sederhana pada saat yang bersamaan.

Kita bisa menempuh jalan tinju implisit - seperti yang dilakukan interface{} saat ini. Tapi saya tidak berpikir bahwa ini memberikan manfaat yang cukup - ini masih terlihat seperti tipe antarmuka yang dimuliakan. Mungkin semacam cek vet dapat dikembangkan sebagai gantinya?

Pengumpul sampah perlu membaca bit tag dari serikat untuk menentukan tata letak. Ini bukan tidak mungkin tetapi akan menjadi perubahan besar pada runtime yang mungkin memperlambat gc.

Mungkin semacam pemeriksaan dokter hewan dapat dikembangkan sebagai gantinya?

https://github.com/BurntSushi/go-sumtype

Pengumpul sampah perlu membaca bit tag dari serikat untuk menentukan tata letak.

Itu ras yang sama persis yang ada dengan antarmuka, ketika mereka bisa berisi non-pointer. Desain itu secara eksplisit dipindahkan.

go-sumtype menarik, terima kasih. Tetapi apa yang terjadi jika paket yang sama mendefinisikan dua tipe serikat?

Kompiler dapat mengimplementasikan tipe serikat pekerja secara internal sebagai antarmuka, tetapi menambahkan sintaks yang seragam dan pemeriksaan tipe standar.

Jika ada N proyek yang menggunakan tipe serikat pekerja, masing-masing berbeda dan dengan N cukup besar, mungkin memperkenalkan satu cara untuk melakukannya mungkin merupakan solusi terbaik.

Tetapi apa yang terjadi jika paket yang sama mendefinisikan dua tipe serikat?

Tidak banyak? Logikanya adalah per-jenis dan menggunakan metode dummy untuk mengenali pelaksana. Cukup gunakan nama yang berbeda untuk metode dummy.

@skybrian IIRC bitmap saat ini yang menentukan tata letak tipe saat ini disimpan di satu tempat. Menambahkan hal seperti itu per objek akan menambah banyak lompatan, dan akan membuat setiap objek opsional menjadi root GC.

Masalah dengan ini, adalah bahwa (setahu saya) tidak ada cara untuk menyimpan tipe pointer (atau tipe yang berisi pointer, seperti string) dan tipe non-pointer bersama-sama

Saya tidak percaya ini perlu. Kompiler dapat tumpang tindih dengan tata letak untuk tipe ketika peta penunjuk cocok, dan bukan sebaliknya. Ketika mereka tidak cocok, akan bebas untuk meletakkannya secara berurutan atau menggunakan pendekatan pointer seperti yang digunakan untuk antarmuka saat ini. Itu bahkan bisa menggunakan tata letak yang tidak bersebelahan untuk anggota struct.

Tapi saya tidak berpikir bahwa ini memberikan manfaat yang cukup - ini masih terlihat seperti tipe antarmuka yang dimuliakan.

Dalam proposal saya , tipe union adalah _exactly_ tipe antarmuka yang dimuliakan - tipe union hanyalah subset dari antarmuka yang hanya diizinkan untuk menyimpan kumpulan tipe yang disebutkan. Ini berpotensi memberi kompiler kebebasan untuk memilih metode penyimpanan yang lebih efisien untuk set tipe tertentu, tetapi itu adalah detail implementasi, bukan motivasi utama.

@rogpeppe - Karena penasaran, dapatkah saya menggunakan tipe sum secara langsung atau apakah saya secara eksplisit perlu melemparkannya ke tipe yang dikenal untuk melakukan sesuatu dengannya? Karena jika saya harus terus-menerus melemparkannya ke tipe yang dikenal, saya benar-benar tidak tahu manfaat apa yang diberikannya daripada apa yang sudah diberikan kepada kita dengan antarmuka. Manfaat utama yang saya lihat adalah pemeriksaan kesalahan waktu kompilasi, semacam, karena unmarshaling masih akan terjadi saat runtime, yang lebih mungkin terjadi di mana Anda akan melihat masalah dengan jenis yang tidak valid yang diteruskan. Manfaat lainnya adalah antarmuka yang lebih terbatas, yang menurut saya tidak memerlukan perubahan bahasa.

Bisakah saya melakukan

type FooType int | float64

func AddOne(foo FooType) FooType {
    return foo + 1
}

// if this can be done, what happens here?
type FooType nil | int
func AddOne(foo FooType) FooType {
    return foo + 1
}

Jika ini tidak dapat dilakukan, saya tidak melihat banyak perbedaan dengan

type FooType interface{}

func AddOne(foo FooType) (FooType, error) {
    switch v := foo.(type) {
        case int:
              return v + 1, nil
        case float64:
              return v + 1.0, nil
    }

    return nil, fmt.Errorf("invalid type %T", foo)
}

// versus
type FooType int | float64

func AddOne(foo FooType) FooType {
    switch v := foo.(type) {
        case int:
              return v + 1
        case float64:
              return v + 1.0
    }

    // assumes the compiler knows that there is no other type is 
    // valid and thus this should always returns a value
    // Would the compiler error out on incomplete switch types?
}

@xibz

Karena penasaran, dapatkah saya menggunakan tipe sum secara langsung atau apakah saya perlu secara eksplisit melemparkannya ke tipe yang dikenal untuk melakukan sesuatu dengannya? Karena jika saya harus terus-menerus melemparkannya ke tipe yang dikenal, saya benar-benar tidak tahu manfaat apa yang diberikannya daripada apa yang sudah diberikan kepada kita dengan antarmuka.

@rogpeppe , mohon
Harus selalu melakukan pencocokan pola (begitulah "casting" disebut saat bekerja dengan tipe sum dalam bahasa pemrograman fungsional) sebenarnya adalah salah satu manfaat terbesar menggunakan tipe sum. Memaksa pengembang untuk secara eksplisit menangani semua bentuk yang mungkin dari tipe penjumlahan adalah cara untuk mencegah pengembang menggunakan variabel yang mengira itu adalah tipe yang diberikan padahal sebenarnya berbeda. Contoh yang dilebih-lebihkan adalah, dalam JavaScript:

const a = "1" // string "1"
const b = a + 5 // string "15" and not number 6

Jika ini tidak dapat dilakukan, saya tidak melihat banyak perbedaan dengan

Saya pikir Anda menyatakan beberapa keuntungan sendiri bukan?

Manfaat utama yang saya lihat adalah pemeriksaan kesalahan waktu kompilasi, semacam, karena unmarshaling masih akan terjadi saat runtime, yang lebih mungkin terjadi di mana Anda akan melihat masalah dengan jenis yang tidak valid yang diteruskan. Manfaat lainnya adalah antarmuka yang lebih terbatas, yang menurut saya tidak memerlukan perubahan bahasa.

// Would the compiler error out on incomplete switch types?

Berdasarkan apa yang dilakukan bahasa pemrograman fungsional, saya pikir ini harus dimungkinkan dan dapat dikonfigurasi 👍

@xibz juga kinerja karena itu dapat dilakukan pada waktu kompilasi vs runtime, tetapi kemudian ada obat generik, semoga, suatu hari sebelum saya mati.

@xibz

Karena penasaran, dapatkah saya menggunakan tipe sum secara langsung atau apakah saya perlu secara eksplisit melemparkannya ke tipe yang dikenal untuk melakukan sesuatu dengannya?

Anda dapat memanggil metode di dalamnya jika semua anggota tipe berbagi metode itu.

Mengambil int | float64 sebagai contoh, apa hasil dari:

var x int|float64 = int(2)
var y int|float64 = float64(0.5)
fmt.Println(x * y)

Apakah itu akan melakukan konversi implisit dari int menjadi float64 ? Atau dari float64 hingga int . Atau akan panik?

Jadi Anda hampir benar - Anda harus mengetik-periksa sebelum menggunakannya dalam banyak kasus. Saya percaya itu keuntungan, bukan kerugian sekalipun.

Keuntungan runtime mungkin signifikan, BTW. Untuk melanjutkan dengan tipe contoh Anda, sepotong tipe [](int|float64) tidak perlu berisi pointer apa pun karena dimungkinkan untuk mewakili semua instans tipe dalam beberapa byte (mungkin 16 byte karena pembatasan penyelarasan), yang dapat menyebabkan peningkatan kinerja yang signifikan dalam beberapa kasus.

Pencocokan pola

Ini agak dibuat-buat, tetapi misalnya jika Anda memiliki pohon sintaks ekspresi, untuk mencocokkan persamaan kuadrat, Anda dapat melakukan sesuatu seperti:

match Add(Add(Mult(Const(a), Power(Var(x), 2)), Mult(Const(b), Var(x))), Const(c)) {
  // here a, b, c are bound to the constants and x is bound to the variable name.
  // x must have been the same in both var expressions or it wouldn't match.
}

Contoh sederhana yang hanya masuk satu level tidak akan menunjukkan perbedaan besar, tapi di sini kita naik ke lima level yang akan cukup rumit untuk dilakukan dengan sakelar tipe bersarang. Bahasa dengan pencocokan pola dapat mencapai beberapa level sekaligus memastikan Anda tidak melewatkan kasus apa pun.

Saya tidak yakin berapa banyak yang muncul di luar kompiler.

@xibz
Manfaat dari tipe sum adalah Anda dan kompiler sama-sama tahu persis tipe apa yang bisa ada di dalam penjumlahan. Itu pada dasarnya perbedaannya. Dengan antarmuka kosong, Anda harus selalu khawatir dan waspada terhadap penyalahgunaan dalam api, dengan selalu memiliki cabang yang tujuan utamanya adalah untuk memulihkan ketika pengguna memberi Anda jenis yang tidak Anda harapkan.

Karena tampaknya ada sedikit harapan untuk tipe sum untuk diimplementasikan dalam kompiler, saya berharap setidaknya arahan komentar standar, seperti //go:union A | B | C diusulkan dan didukung oleh go vet .

Dengan cara standar untuk mendeklarasikan tipe penjumlahan, setelah N tahun dimungkinkan untuk mengetahui berapa banyak paket yang menggunakannya.

Dengan draf desain terbaru untuk obat generik, mungkin jumlah tipe dapat dikaitkan dengannya.

Di salah satu draf muncul gagasan untuk menggunakan antarmuka alih-alih kontrak, dan antarmuka harus mendukung daftar tipe:

type Foo interface { 
     int64, int32, int, uint, uint32, uint64
}

Meskipun itu sendiri tidak akan menghasilkan serikat yang dikemas dengan memori, tetapi mungkin ketika menggunakan dalam fungsi atau struct generik, itu tidak akan dikotak, dan setidaknya akan memberikan keamanan tipe ketika berhadapan dengan daftar tipe yang terbatas.

Dan mungkin, menggunakan antarmuka khusus ini dalam sakelar tipe akan mengharuskan sakelar seperti itu menjadi lengkap.

Ini bukan sintaks pendek yang ideal (misalnya: Foo | int32 | []Bar ), tetapi ini adalah sesuatu.

Dengan draf desain terbaru untuk obat generik, mungkin jumlah tipe dapat dikaitkan dengannya.

Di salah satu draf muncul gagasan untuk menggunakan antarmuka alih-alih kontrak, dan antarmuka harus mendukung daftar tipe:

type Foo interface { 
     int64, int32, int, uint, uint32, uint64
}

Meskipun itu sendiri tidak akan menghasilkan serikat yang dikemas dengan memori, tetapi mungkin ketika menggunakan dalam fungsi atau struct generik, itu tidak akan dikotak, dan setidaknya akan memberikan keamanan tipe ketika berhadapan dengan daftar tipe yang terbatas.

Dan mungkin, menggunakan antarmuka khusus ini dalam sakelar tipe akan mengharuskan sakelar seperti itu menjadi lengkap.

Ini bukan sintaks pendek yang ideal (misalnya: Foo | int32 | []Bar ), tetapi ini adalah sesuatu.

Cukup mirip dengan proposal saya: https://github.com/golang/go/issues/19412#issuecomment -520306000

type foobar union {
  int
  float
}

@mathieudevos wow, sebenarnya saya sangat menyukainya.

Bagi saya, keanehan terbesar ( satu - cocok . Kemudian Anda berakhir dengan beberapa antarmuka yang hanya dapat Anda gunakan sebagai batasan parameter tipe, dan seterusnya...

Konsep union bekerja dengan baik dalam pikiran saya karena Anda kemudian dapat menyematkan union dalam interface untuk mencapai "batasan yang mencakup metode dan tipe mentah". Antarmuka terus berfungsi apa adanya, dan dengan semantik yang didefinisikan di sekitar gabungan, mereka dapat digunakan dalam kode biasa dan perasaan aneh hilang.

// Ordinary interface
type Stringer interface {
    String() string
}

// Type union
type Foobar union {
    int
    float
}

// Equivalent to an interface with a type list
type FoobarStringer interface {
    Stringer
    Foobar
}

// Unions can intersect
type SuperFoo union {
    Foobar
    int
}

// Doesn't compile since these unions can't intersect
type Strange interface {
    Foobar
    union {
        int
        string
    }
}

EDIT - Sebenarnya, baru saja melihat CL ini: https://github.com/golang/go/commit/af48c2e84b52f99d30e4787b1b8d527b5cd2ab64

Manfaat utama dari perubahan ini adalah membuka pintu untuk umum
(non-kendala) penggunaan antarmuka dengan daftar tipe

...Besar! Antarmuka menjadi dapat digunakan sepenuhnya sebagai tipe penjumlahan, yang menyatukan semantik di seluruh penggunaan reguler dan batasan. (Jelas belum dihidupkan, tapi saya pikir itu adalah tujuan yang bagus untuk dituju.)

Saya telah membuka #41716 untuk membahas cara versi tipe jumlah muncul dalam draf desain generik saat ini.

Saya hanya ingin berbagi proposal lama dari @henryas tentang Tipe Data Aljabar. Ini ditulis dengan sangat baik dengan kasus penggunaan yang disediakan.
https://github.com/golang/go/issues/21154
Sayangnya, telah ditutup oleh @mvdan pada hari yang sama tanpa penghargaan apa pun atas karya tersebut. Saya cukup yakin orang itu benar-benar merasa seperti itu dan dengan demikian tidak ada aktivitas lebih lanjut di akun gh. Aku merasa kasihan pada pria itu.

Saya sangat suka #21154. Tampaknya menjadi hal yang berbeda (dan karenanya komentar @mvdan ) menutupnya sebagai penipuan tidak cukup memukul. Buka kembali di sana atau termasuk dalam diskusi di sini?

Ya, saya benar-benar ingin memiliki kemampuan untuk memodelkan beberapa logika bisnis tingkat tinggi dengan cara yang sama seperti yang dijelaskan dalam masalah itu. Jumlah jenis untuk enum-seperti, opsi terbatas, dan jenis yang diterima yang disarankan seperti pada masalah lain akan luar biasa di kotak alat. Kode bisnis/domain di Go terkadang terasa agak kikuk saat ini.

Satu-satunya umpan balik saya adalah bahwa type foo,bar di dalam antarmuka terlihat agak canggung dan kelas dua, dan saya setuju bahwa harus ada pilihan antara nullable dan non-nullable (jika memungkinkan).

@ProximaB Saya tidak mengerti mengapa Anda mengatakan "tidak ada aktivitas lebih lanjut di akun gh". Sejak itu mereka telah membuat dan mengomentari banyak masalah lain juga, banyak di antaranya tentang proyek Go. Saya tidak melihat bukti bahwa aktivitas mereka telah dipengaruhi oleh masalah itu sama sekali.

Selanjutnya, saya sangat setuju dengan Daniel menutup masalah itu sebagai penipuan yang satu ini. Saya tidak mengerti mengapa @andig mengatakan bahwa mereka mengusulkan sesuatu yang berbeda. Sejauh yang saya dapat memahami teks #21154, itu mengusulkan hal yang persis sama yang kita diskusikan di sini dan saya tidak akan terkejut sama sekali jika bahkan sintaks yang tepat sudah disarankan di suatu tempat di megathread ini (semantik, sejauh dijelaskan, pasti. Beberapa kali). Bahkan, saya akan melangkah lebih jauh dengan mengatakan penutupan Daniels terbukti benar dengan panjangnya masalah ini, karena sudah berisi diskusi yang cukup rinci dan bernuansa #2154, jadi mengulangi semua itu akan sulit dan berlebihan.

Saya setuju dan mengerti bahwa mungkin mengecewakan jika proposal ditutup sebagai penipuan. Tapi saya tidak tahu cara praktis untuk menghindarinya. Melakukan diskusi di satu tempat tampaknya bermanfaat bagi semua orang yang terlibat dan menjaga banyak masalah untuk hal yang sama tetap terbuka, tanpa diskusi apa pun tentangnya, jelas tidak ada gunanya.

Selanjutnya, saya sangat setuju dengan Daniel menutup masalah itu sebagai penipuan yang satu ini. Saya tidak mengerti mengapa @andig mengatakan bahwa mereka mengusulkan sesuatu yang berbeda. Sejauh yang saya bisa mengerti teks #21154, itu mengusulkan hal yang persis sama yang sedang kita diskusikan di sini

Membaca ulang masalah ini saya setuju. Sepertinya saya bingung dengan masalah ini dengan kontrak obat generik. Saya akan sangat mendukung tipe jumlah. Saya tidak bermaksud kasar, mohon maaf jika terkesan seperti itu.

Saya seorang manusia dan masalah berkebun kadang-kadang bisa rumit, jadi tentu saja tunjukkan ketika saya membuat kesalahan :) Tetapi dalam kasus ini saya berpikir bahwa proposal jenis jumlah tertentu harus diambil dari utas ini seperti https:/ /github.com/golang/go/issues/19412#issuecomment -701625548

Saya seorang manusia dan masalah berkebun kadang-kadang bisa rumit, jadi tentu saja tunjukkan ketika saya membuat kesalahan :) Tetapi dalam kasus ini saya berpikir bahwa proposal jenis jumlah tertentu harus diambil dari utas ini seperti

@mvdan bukan manusia. Percaya padaku. Saya tetangganya. Hanya bercanda.

Terimakasih atas perhatiannya. Saya tidak terlalu terikat dengan proposal saya. Jangan ragu untuk memotong, memodifikasi, dan menembak jatuh bagian mana pun dari mereka. Saya sibuk di kehidupan nyata, jadi saya tidak punya kesempatan untuk aktif dalam diskusi. Senang mengetahui bahwa orang-orang membaca proposal saya dan beberapa benar-benar menyukainya.

Maksud aslinya adalah untuk memungkinkan pengelompokan tipe berdasarkan relevansi domainnya, di mana mereka tidak harus berbagi perilaku yang sama, dan meminta kompiler menegakkannya. Menurut pendapat saya, ini hanya masalah verifikasi statis, yang dilakukan selama kompilasi. Tidak perlu bagi kompiler untuk menghasilkan kode yang mempertahankan hubungan kompleks antar tipe. Kode yang dihasilkan mungkin memperlakukan tipe domain ini secara normal seolah-olah mereka adalah tipe{} antarmuka reguler. Perbedaannya adalah bahwa kompiler sekarang melakukan pemeriksaan tipe statis tambahan saat kompilasi. Itu pada dasarnya adalah inti dari proposal saya #21154

@henryas Senang bertemu denganmu! 😊
Saya bertanya-tanya apakah Golang tidak menggunakan pengetikan bebek yang membuat hubungan antar tipe jauh lebih ketat dan memungkinkan pengelompokan objek berdasarkan relevansi domainnya seperti yang Anda jelaskan dalam proposal Anda.

@henryas Senang bertemu denganmu! 😊
Saya bertanya-tanya apakah Golang tidak menggunakan pengetikan bebek yang membuat hubungan antar tipe jauh lebih ketat dan memungkinkan pengelompokan objek berdasarkan relevansi domainnya seperti yang Anda jelaskan dalam proposal Anda.

Itu akan, tapi itu akan melanggar janji kompatibilitas dengan Go 1. Kita mungkin tidak membutuhkan tipe jumlah jika kita memiliki antarmuka eksplisit. Namun, mengetik bebek belum tentu merupakan hal yang buruk. Itu membuat hal-hal tertentu lebih ringan dan nyaman. Saya menikmati mengetik bebek. Ini adalah masalah menggunakan alat yang tepat untuk pekerjaan itu.

@henryas saya setuju. Itu adalah pertanyaan hipotetis. Pembuat konten Go pasti sangat mempertimbangkan semua suka dan duka.
Di sisi lain, panduan pengkodean seperti memverifikasi kepatuhan antarmuka tidak akan pernah muncul.
https://github.com/uber-go/guide/blob/master/style.md#verify -interface-compliance

Bisakah Anda melakukan diskusi di luar topik ini di tempat lain? Ada banyak orang yang berlangganan masalah ini.
Kepuasan antarmuka terbuka telah menjadi bagian dari Go sejak awal dan tidak akan berubah.

Apakah halaman ini membantu?
0 / 5 - 0 peringkat

Masalah terkait

stub42 picture stub42  ·  3Komentar

rakyll picture rakyll  ·  3Komentar

natefinch picture natefinch  ·  3Komentar

enoodle picture enoodle  ·  3Komentar

OneOfOne picture OneOfOne  ·  3Komentar