Apa perbedaan ciri-ciri Karat dengan Go Interfaces?

64

Saya relatif akrab dengan Go, setelah menulis sejumlah program kecil di dalamnya. Karat, tentu saja, saya kurang terbiasa tetapi mengawasi.

Setelah baru-baru ini membaca http://yager.io/programming/go.html , saya pikir saya akan secara pribadi memeriksa dua cara Generics ditangani karena artikel tersebut kelihatannya secara tidak adil mengkritik Go ketika, dalam praktiknya, tidak ada banyak antarmuka. tidak bisa menyelesaikan dengan elegan. Saya terus mendengar hype tentang betapa kuatnya Karakter Rust dan tidak ada apa-apa selain kritik dari orang-orang tentang Go. Setelah memiliki pengalaman dalam Go, saya bertanya-tanya seberapa benar itu dan apa perbedaan akhirnya. Apa yang saya temukan adalah bahwa Ciri dan Antarmuka sangat mirip! Pada akhirnya, saya tidak yakin apakah saya melewatkan sesuatu, jadi di sini adalah ikhtisar pendidikan singkat tentang kesamaan mereka sehingga Anda dapat memberi tahu saya apa yang saya lewatkan!

Sekarang, mari kita lihat Go Interfaces dari dokumentasinya :

Antarmuka di Go menyediakan cara untuk menentukan perilaku objek: jika sesuatu dapat melakukan ini, maka itu dapat digunakan di sini.

Sejauh ini antarmuka Stringeryang paling umum adalah yang mengembalikan string yang mewakili objek.

type Stringer interface {
    String() string
}

Jadi, objek apa pun yang telah String()didefinisikan di atasnya adalah Stringerobjek. Ini dapat digunakan dalam jenis tanda tangan sehingga func (s Stringer) print()mengambil hampir semua objek dan mencetaknya.

Kami juga memiliki interface{}yang mengambil objek apa pun. Kami kemudian harus menentukan jenis saat runtime melalui refleksi.


Sekarang, mari kita lihat Karat Karakter dari dokumentasi mereka :

Paling sederhana, suatu sifat adalah seperangkat tanda tangan metode nol atau lebih. Sebagai contoh, kita dapat mendeklarasikan sifat Printable untuk hal-hal yang dapat dicetak ke konsol, dengan tanda tangan metode tunggal:

trait Printable {
    fn print(&self);
}

Ini segera terlihat sangat mirip dengan Antarmuka Go kami. Satu-satunya perbedaan yang saya lihat adalah kita mendefinisikan 'Implementasi' Traits daripada hanya mendefinisikan metode. Jadi kita lakukan

impl Printable for int {
    fn print(&self) { println!("{}", *self) }
}

dari pada

fn print(a: int) { ... }

Pertanyaan Bonus: Apa yang terjadi di Rust jika Anda mendefinisikan fungsi yang mengimplementasikan suatu sifat tetapi Anda tidak menggunakannya impl? Itu tidak berhasil?

Tidak seperti Go's Interfaces, sistem tipe Rust memiliki parameter tipe yang memungkinkan Anda melakukan generik yang tepat dan hal-hal seperti interface{}ketika kompiler dan runtime benar-benar mengetahui jenisnya. Sebagai contoh,

trait Seq<T> {
    fn length(&self) -> uint;
}

bekerja pada semua jenis dan kompiler tahu bahwa jenis elemen Sequence pada waktu kompilasi daripada menggunakan refleksi.


Sekarang, pertanyaan aktual: apakah saya kehilangan perbedaan di sini? Apakah mereka benar-benar yang serupa? Apakah tidak ada perbedaan mendasar yang saya lewatkan di sini? (Dalam penggunaan. Detail implementasi menarik, tetapi pada akhirnya tidak penting jika fungsinya sama.)

Selain perbedaan sintaksis, perbedaan aktual yang saya lihat adalah:

  1. Go memiliki pengiriman metode otomatis vs. Rust yang memerlukan (?) implS untuk mengimplementasikan Trait
    • Elegan vs. Eksplisit
  2. Karat memiliki parameter tipe yang memungkinkan generik yang tepat tanpa refleksi.
    • Go benar-benar tidak memiliki respons di sini. Ini adalah satu-satunya hal yang secara signifikan lebih kuat dan pada akhirnya hanya penggantian untuk metode menyalin dan menempel dengan tanda tangan jenis yang berbeda.

Apakah ini satu-satunya perbedaan yang tidak sepele? Jika demikian, akan muncul sistem Go's Interface / Type, dalam praktiknya, tidak selemah yang dirasakan.

Logan
sumber

Jawaban:

59

Apa yang terjadi di Rust jika Anda mendefinisikan fungsi yang mengimplementasikan suatu sifat tetapi Anda tidak menggunakan impl? Itu tidak berhasil?

Anda perlu menerapkan sifat tersebut secara eksplisit; Kebetulan memiliki metode dengan nama / tanda tangan yang cocok tidak ada artinya bagi Rust.

Pengiriman panggilan umum

Apakah ini satu-satunya perbedaan yang tidak sepele? Jika demikian, akan muncul sistem Go's Interface / Type, dalam praktiknya, tidak selemah yang dirasakan.

Tidak menyediakan pengiriman statis dapat menjadi hit kinerja yang signifikan untuk kasus-kasus tertentu (misalnya yang Iteratorsaya sebutkan di bawah). Saya pikir ini yang Anda maksud dengan

Go benar-benar tidak memiliki respons di sini. Ini adalah satu-satunya hal yang secara signifikan lebih kuat dan pada akhirnya hanya penggantian untuk metode menyalin dan menempel dengan tanda tangan jenis yang berbeda.

tapi saya akan membahasnya lebih detail, karena ada baiknya memahami perbedaannya secara mendalam.

Di Rust

Pendekatan Rust memungkinkan pengguna untuk memilih antara pengiriman statis dan pengiriman dinamis . Sebagai contoh, jika sudah

trait Foo { fn bar(&self); }

impl Foo for int { fn bar(&self) {} }
impl Foo for String { fn bar(&self) {} }

fn call_bar<T: Foo>(value: T) { value.bar() }

fn main() {
    call_bar(1i);
    call_bar("foo".to_string());
}

maka dua call_barpanggilan di atas akan dikompilasi untuk panggilan ke, masing-masing,

fn call_bar_int(value: int) { value.bar() }
fn call_bar_string(value: String) { value.bar() }

di mana .bar()panggilan metode tersebut adalah panggilan fungsi statis, yaitu ke alamat fungsi tetap di memori. Ini memungkinkan untuk optimisasi seperti inlining, karena kompiler tahu persis fungsi mana yang dipanggil. (Ini juga yang dilakukan C ++, kadang-kadang disebut "monomorphisation".)

Dalam Go

Go hanya memungkinkan pengiriman dinamis untuk fungsi "generik", yaitu alamat metode diambil dari nilai dan dipanggil dari sana, sehingga fungsi yang tepat hanya diketahui saat runtime. Menggunakan contoh di atas

type Foo interface { bar() }

func call_bar(value Foo) { value.bar() }

type X int;
type Y string;
func (X) bar() {}
func (Y) bar() {}

func main() {
    call_bar(X(1))
    call_bar(Y("foo"))
}

Sekarang, kedua call_bars akan selalu memanggil di atas call_bar, dengan alamat bardiambil dari antarmuka vtable .

Level rendah

Untuk mengulangi kata-kata di atas, dalam notasi C. Versi Rust menciptakan

/* "implementing" the trait */
void bar_int(...) { ... }
void bar_string(...) { ... }

/* the monomorphised `call_bar` function */
void call_bar_int(int value) {
    bar_int(value);
}
void call_bar_string(string value) {
    bar_string(value);
}

int main() {
    call_bar_int(1);
    call_bar_string("foo");
    // pretend that is the (hypothetical) `string` type, not a `char*`
    return 1;
}

Untuk Go, ini sesuatu yang lebih seperti:

/* implementing the interface */
void bar_int(...) { ... }
void bar_string(...) { ... }

// the Foo interface type
struct Foo {
    void* data;
    struct FooVTable* vtable;
}
struct FooVTable {
    void (*bar)(void*);
}

void call_bar(struct Foo value) {
    value.vtable.bar(value.data);
}

static struct FooVTable int_vtable = { bar_int };
static struct FooVTable string_vtable = { bar_string };

int main() {
    int* i = malloc(sizeof *i);
    *i = 1;
    struct Foo int_data = { i, &int_vtable };
    call_bar(int_data);

    string* s = malloc(sizeof *s);
    *s = "foo"; // again, pretend the types work
    struct Foo string_data = { s, &string_vtable };
    call_bar(string_data);
}

(Ini tidak sepenuhnya benar --- harus ada lebih banyak informasi di vtable --- tetapi pemanggilan metode menjadi penunjuk fungsi dinamis adalah hal yang relevan di sini.)

Rust menawarkan pilihan

Kembali ke

Pendekatan Rust memungkinkan pengguna untuk memilih antara pengiriman statis dan pengiriman dinamis.

Sejauh ini saya hanya menunjukkan Rust memiliki generik yang dikirim secara statis, tetapi Rust dapat memilih yang dinamis seperti Go (dengan implementasi yang pada dasarnya sama), melalui objek trait. Notated like &Foo, yang merupakan referensi pinjam untuk tipe yang tidak dikenal yang mengimplementasikan Foosifat tersebut. Nilai-nilai ini memiliki representasi vtable yang sama / sangat mirip dengan objek antarmuka Go. (Objek sifat adalah contoh dari "tipe eksistensial" .)

Ada beberapa contoh di mana pengiriman dinamis sangat membantu (dan kadang-kadang lebih berkinerja, dengan, misalnya mengurangi kembung / duplikasi kode), tetapi pengiriman statis memungkinkan kompiler untuk menyejajarkan panggil dan menerapkan semua optimisasi mereka, yang berarti biasanya lebih cepat. Ini sangat penting untuk hal-hal seperti protokol iterasi Rust , di mana panggilan metode pengiriman statis memungkinkan untuk iterator menjadi secepat setara dengan C, sementara masih tampak tingkat tinggi dan ekspresif .

Tl; dr: Pendekatan Rust menawarkan pengiriman statis dan dinamis dalam obat generik, sesuai kebijaksanaan pemrogram; Go hanya memungkinkan untuk pengiriman dinamis.

Polimorfisme parametrik

Lebih jauh, menekankan ciri-ciri dan merenungkan refleksi memberi Rust polimorfisme parametrik yang jauh lebih kuat : pemrogram tahu persis apa yang bisa dilakukan fungsi dengan argumennya, karena harus mendeklarasikan ciri-ciri yang diterapkan oleh tipe generik dalam tanda tangan fungsi.

Pendekatan Go sangat fleksibel, tetapi memiliki lebih sedikit jaminan bagi penelepon (membuatnya agak sulit bagi programmer untuk memikirkan), karena internal fungsi dapat (dan melakukan) permintaan informasi jenis tambahan (ada bug di Go perpustakaan standar di mana, iirc, sebuah fungsi yang mengambil penulis akan menggunakan refleksi untuk memanggil Flushbeberapa input, tetapi tidak yang lain).

Membangun abstraksi

Ini agak menyakitkan, jadi saya hanya akan berbicara sebentar, tetapi memiliki generik "layak" seperti Rust telah memungkinkan tipe data tingkat rendah seperti Go mapdan []untuk benar-benar diimplementasikan secara langsung di perpustakaan standar dengan cara yang sangat aman, dan ditulis dalam Rust ( HashMapdan Vecmasing - masing).

Dan bukan hanya tipe-tipe itu, Anda dapat membangun struktur generik yang aman untuk tipe di atasnya, misalnya LruCachelapisan caching generik di atas sebuah hashmap. Ini berarti orang hanya dapat menggunakan struktur data langsung dari perpustakaan standar, tanpa harus menyimpan data sebagai interface{}dan menggunakan pernyataan tipe ketika memasukkan / mengekstraksi. Artinya, jika Anda memiliki LruCache<int, String>, Anda dijamin bahwa kunci selalu ints dan nilainya selalu Strings: tidak ada cara untuk secara tidak sengaja memasukkan nilai yang salah (atau mencoba mengekstraksi yang bukan String).

huon
sumber
Saya sendiri AnyMapadalah demonstrasi yang baik dari kekuatan Rust, menggabungkan objek-objek sifat dengan obat-obatan generik untuk memberikan abstraksi yang aman dan ekspresif dari hal rapuh yang harus ditulis oleh Go map[string]interface{}.
Chris Morgan
Seperti yang saya harapkan, Rust lebih kuat dan menawarkan lebih banyak pilihan secara native / elegan, tetapi sistem Go cukup dekat sehingga sebagian besar hal yang terlewat dapat diselesaikan dengan hack kecil seperti interface{}. Sementara Rust tampaknya secara teknis lebih unggul, saya masih berpikir kritik terhadap Go ... agak terlalu keras. Kekuatan programmer cukup setara untuk 99% tugas.
Logan
22
@Logan, untuk domain level rendah / kinerja tinggi yang dituju oleh Rust (mis. Sistem operasi, browser web ... inti program pemrograman "sistem"), tidak memiliki opsi pengiriman statis (dan kinerja yang diberikan / optimalisasi itu memungkinkan) tidak dapat diterima. Ini adalah salah satu alasan mengapa Go tidak cocok dengan Rust untuk aplikasi semacam itu. Bagaimanapun, kekuatan programmer tidak benar-benar setara, Anda kehilangan (waktu kompilasi) keamanan jenis untuk setiap struktur data yang dapat digunakan kembali dan tidak built-in, jatuh kembali ke pernyataan tipe runtime.
huon
10
Benar sekali - Rust menawarkan Anda lebih banyak kekuatan. Saya menganggap Rust sebagai C ++ yang aman, dan Go sebagai Python cepat (atau Java yang sangat disederhanakan). Untuk sebagian besar tugas di mana produktivitas pengembang paling penting (dan hal-hal seperti runtime dan pengumpulan sampah tidak bermasalah), pilih Go (misalnya, server web, sistem bersamaan, utilitas baris perintah, aplikasi pengguna, dll). Jika Anda membutuhkan setiap bit kinerja terakhir (dan produktivitas pengembang menjadi terkutuk), pilih Rust (misalnya, browser, sistem operasi, sistem embedded yang dibatasi sumber daya).
weberc2