Apa perbedaan antara kelas tipe Haskell dan antarmuka Go?

8

Saya bertanya-tanya apakah ada perbedaan antara kelas tipe Haskell dan antarmuka Go. Keduanya mendefinisikan tipe berdasarkan fungsi, dengan cara itu, bahwa suatu nilai cocok dengan suatu tipe, jika suatu fungsi yang dibutuhkan oleh tipe tersebut didefinisikan untuk nilai tersebut.

Apakah ada perbedaan atau hanya dua nama untuk hal yang sama?

ceving
sumber

Jawaban:

6

Kedua konsep ini sangat mirip. Dalam bahasa OOP normal, kami melampirkan vtable (atau untuk interface: itable) ke setiap objek:

| this
v
+---+---+---+
| V | a | b | the object with fields a, b
+---+---+---+
  |
  v
 +---+---+---+
 | o | p | q | the vtable with method slots o(), p(), q()
 +---+---+---+

Ini memungkinkan kami untuk memanggil metode yang mirip dengan this->vtable.p(this).

Di Haskell, tabel metode lebih seperti argumen tersembunyi implisit:

method :: Class a => a -> a -> Int

akan terlihat seperti fungsi C ++

template<typename A>
int method(Class<A>*, A*, A*)

di mana Class<A>adalah instance dari typeclass Classuntuk tipe A. Metode akan dipanggil seperti

typeclass_instance->p(value_ptr);

Instance terpisah dari nilai-nilai. Nilai masih mempertahankan tipe yang sebenarnya. Meskipun typeclasses memungkinkan polimorfisme, ini bukan subtipe polimorfisme. Itu membuatnya mustahil untuk membuat daftar nilai yang memuaskan a Class. Misalnya dengan asumsi kita memiliki instance Class Int ...dan instance Class String ..., kita tidak dapat membuat tipe daftar yang heterogen seperti [Class]yang memiliki nilai seperti [42, "foo"]. (Ini dimungkinkan ketika Anda menggunakan ekstensi "tipe eksistensial", yang secara efektif beralih ke pendekatan Go).

Di Go, nilai tidak mengimplementasikan antarmuka tetap. Akibatnya tidak dapat memiliki pointer vtable. Sebagai gantinya, pointer ke tipe antarmuka diimplementasikan sebagai fat pointer yang mencakup satu pointer ke data, pointer lain ke itable:

    `this` fat pointer
    +---+---+
    |   |   |
    +---+---+
 ____/    \_________
v                   v
+---+---+---+       +---+---+
| o | p | q |       | a | b | the data with
+---+---+---+       +---+---+ fields a, b
itable with method
slots o(), p(), q()

this.itable->p(this.data_ptr)

Itable digabungkan dengan data menjadi penunjuk gemuk saat Anda melakukan cast dari nilai biasa ke tipe antarmuka. Setelah Anda memiliki tipe antarmuka, tipe data yang sebenarnya telah menjadi tidak relevan. Bahkan, Anda tidak dapat mengakses bidang secara langsung tanpa melalui metode atau menurunkan antarmuka (yang mungkin gagal).

Pendekatan Go untuk pengiriman antarmuka dikenakan biaya: setiap pointer polimorfik dua kali lebih besar dari pointer normal. Juga, casting dari satu antarmuka ke antarmuka yang lain melibatkan menyalin pointer metode ke vtable baru. Tapi begitu kita sudah membangun itable, ini memungkinkan kita untuk mengirim panggilan metode ke banyak antarmuka dengan murah, sesuatu yang menderita dengan bahasa OOP tradisional. Di sini, m adalah jumlah metode dalam antarmuka target, dan b adalah jumlah kelas dasar:

  • C ++ memang objek slicing atau perlu mengejar pointer virtual saat casting, tetapi kemudian memiliki akses vtable sederhana. O (1) atau O (b) biaya pengiriman, tetapi O (1) metode pengiriman.
  • Java Hotspot VM tidak harus melakukan apa pun ketika upcasting, tetapi pada metode antarmuka lookup melakukan pencarian linear melalui semua itables yang diimplementasikan oleh kelas itu. O (1) upcasting, tetapi O (b) metode pengiriman.
  • Python tidak harus melakukan apa pun ketika upcasting, tetapi menggunakan pencarian linear melalui daftar kelas dasar C3-linierisasi. O (1) upcasting, tetapi O (b²) metode pengiriman? Saya tidak yakin apa kompleksitas algoritmik C3.
  • .NET CLR menggunakan pendekatan yang mirip dengan Hotspot tetapi menambahkan tingkat tipuan lain dalam upaya untuk mengoptimalkan penggunaan memori. O (1) upcasting, tetapi O (b) metode pengiriman.

Kompleksitas tipikal untuk pengiriman metode jauh lebih baik karena metode pencarian sering dapat di-cache, tetapi kompleksitas kasus terburuk cukup mengerikan.

Sebagai perbandingan, Go memiliki O (1) atau O (m) upcasting, dan O (1) metode pengiriman. Haskell tidak memiliki upcasting (membatasi tipe dengan kelas tipe adalah efek waktu kompilasi), dan metode pengiriman O (1).

amon
sumber
Terima kasih untuk [42, "foo"]. Ini adalah contoh nyata.
ceving
2
Meskipun jawaban ini ditulis dengan baik dan berisi informasi yang berguna, saya pikir bahwa dengan berfokus pada implementasi dalam kode yang dikompilasi, secara signifikan melebih-lebihkan persamaan antara antarmuka dan kelas tipe. Dengan kelas tipe (dan sistem tipe Haskell lebih umum), sebagian besar hal menarik terjadi selama kompilasi dan tidak tercermin dalam kode mesin akhir.
KA Buhr
7

Ada beberapa perbedaan

  1. Kacamata Haskell diketik secara nominal - Anda harus menyatakan bahwa Maybea Monad. Antar muka Go diketik secara struktural: jika circlemenyatakan area() float64dan demikian juga squarekeduanya berada di bawah antarmuka shapesecara otomatis.
  2. Haskell (dengan ekstensi GHC) memiliki Kelas Tipe Multi Parameter dan (seperti Maybe acontoh saya ) kelas tipe untuk tipe yang lebih tinggi. Go tidak memiliki padanan untuk ini.
  3. Di Haskell, kelas tipe dikonsumsi dengan polimorfisme terikat yang memberi Anda batasan yang tidak dapat diekspresikan dengan Go. Misalnya + :: Num a => a -> a -> a, yang memastikan bahwa Anda tidak akan mencoba menambahkan floating point dan angka empat, tidak dapat diekspresikan di Go.
walpen
sumber
Apakah 1. benar-benar perbedaan atau itu hanya kehilangan gula?
ceving
1
Pergi antarmuka mendefinisikan protokol untuk nilai-nilai, kelas tipe Haskell mendefinisikan protokol untuk jenis, itu juga merupakan perbedaan yang cukup besar, saya akan mengatakan. (Itu sebabnya mereka disebut "kelas tipe", setelah semua. Mereka mengklasifikasikan tipe, tidak seperti kelas OO (atau antarmuka Go), yang mengklasifikasikan nilai-nilai.)
Jörg W Mittag
1
@ceving, ini jelas bukan gula dalam arti normal: jika Haskell melompat ke sesuatu seperti Scala yang tersirat untuk kelas tipe, itu akan merusak banyak kode yang ada.
walpen
@ JörgWMittag Saya akan setuju di sana dan saya menyukai jawaban Anda: Saya mencoba untuk mendapatkan lebih banyak perbedaan dari perspektif pengguna.
walpen
@walpen Mengapa ini memecahkan kode? Saya bertanya-tanya bagaimana kode seperti itu bisa ada mengingat ketatnya sistem tipe Haskell.
ceving
4

Mereka sangat berbeda. Go interface mendefinisikan protokol untuk nilai, kelas tipe Haskell mendefinisikan protokol untuk tipe. (Itu sebabnya mereka disebut "kelas tipe", setelah semua. Mereka mengklasifikasikan tipe, tidak seperti kelas OO (atau antarmuka Go), yang mengklasifikasikan nilai.)

Antarmuka Go hanya membosankan mengetik struktural lama, tidak lebih.

Jörg W Mittag
sumber
1
Bisakah Anda menjelaskannya? Mungkin bahkan tanpa nada merendahkan. Apa yang saya baca menyatakan bahwa kelas tipe adalah polimorfisme ad-hoc yang sebanding dengan overloading operator, yang sama dengan antarmuka di Go.
ceving
Dari tutorial Haskell : "Seperti deklarasi antarmuka, deklarasi kelas Haskell mendefinisikan protokol untuk menggunakan objek"
ceving
2
Dari tutorial yang sama ( huruf tebal penekanan): "Tipe kelas [...] memungkinkan kita untuk menyatakan tipe mana yang merupakan instance dari kelas mana" Instance dari antarmuka Go adalah nilai , instance dari kelas tipe Haskell adalah tipe . Nilai dan tipe hidup dalam dua dunia yang sepenuhnya terpisah (setidaknya dalam bahasa seperti Haskell dan Go, bahasa yang diketik secara dependen seperti Agda, Guru, Epigram, Idris, Isabelle, Coq, dll. Adalah masalah yang berbeda).
Jörg W Mittag
Memilih karena jawabannya berwawasan luas, tetapi saya pikir lebih detail bisa membantu. Dan apa yang membosankan tentang pengetikan struktural ?! Ini sangat langka, dan layak dirayakan menurut pendapat saya.
Max Heiber