Ketik kelas vs antarmuka objek

33

Saya rasa saya tidak mengerti kelas tipe. Saya pernah membaca di suatu tempat bahwa memikirkan kelas tipe sebagai "antarmuka" (dari OO) yang mengimplementasikan tipe adalah salah dan menyesatkan. Masalahnya adalah, saya mengalami masalah melihat mereka sebagai sesuatu yang berbeda dan bagaimana itu salah.

Misalnya, jika saya memiliki kelas tipe (dalam sintaks Haskell)

class Functor f where
  fmap :: (a -> b) -> f a -> f b

Bagaimana itu berbeda dari antarmuka [1] (dalam sintaksis Java)

interface Functor<A> {
  <B> Functor<B> fmap(Function<B, A> fn)
}

interface Function<Return, Argument> {
  Return apply(Argument arg);
}

Satu perbedaan yang mungkin saya pikirkan adalah bahwa implementasi kelas tipe yang digunakan pada doa tertentu tidak ditentukan tetapi ditentukan dari lingkungan - katakanlah, memeriksa modul yang tersedia untuk implementasi untuk tipe ini. Itu tampaknya merupakan artefak implementasi yang dapat diatasi dalam bahasa OO; seperti kompiler (atau runtime) dapat memindai pembungkus / extender / monyet-patcher yang memperlihatkan antarmuka yang diperlukan pada jenisnya.

Apa yang saya lewatkan?

[1] Perhatikan bahwa f aargumen telah dihapus fmapsejak diberikan sebagai bahasa OO, Anda akan memanggil metode ini pada objek. Antarmuka ini menganggap f aargumen telah diperbaiki.

oconnor0
sumber

Jawaban:

46

Dalam bentuk dasarnya, kelas tipe agak mirip dengan antarmuka objek. Namun, dalam banyak hal, mereka jauh lebih umum.

  1. Pengiriman adalah pada jenis, bukan nilai. Tidak diperlukan nilai untuk melakukannya. Misalnya, dimungkinkan untuk melakukan pengiriman pada jenis fungsi hasil, seperti dengan Readkelas Haskell :

    class Read a where
      readsPrec :: Int -> String -> [(a, String)]
      ...
    

    Pengiriman seperti itu jelas tidak mungkin dilakukan pada OO konvensional.

  2. Jenis kelas secara alami diperluas ke beberapa pengiriman, cukup dengan memberikan beberapa parameter:

    class Mul a b c where
      (*) :: a -> b -> c
    
    instance Mul Int Int Int where ...
    instance Mul Int Vec Vec where ...
    instance Mul Vec Vec Int where ...
    
  3. Definisi Instance tidak tergantung dari definisi kelas dan tipe, yang membuatnya lebih modular. Tipe T dari modul A dapat dipasang ke kelas C dari modul M2 tanpa memodifikasi definisi keduanya, cukup dengan memberikan instance pada modul M3. Dalam OO, ini membutuhkan lebih banyak fitur bahasa esoteris (dan lebih sedikit OO-ish) seperti metode ekstensi.

  4. Kelas tipe didasarkan pada polimorfisme parametrik, bukan subtipe. Itu memungkinkan pengetikan yang lebih akurat. Pertimbangkan misalnya

    pick :: Enum a => a -> a -> a
    pick x y = if fromEnum x == 0 then y else x
    

    vs.

    pick(x : Enum, y : Enum) : Enum = if x.fromEnum() == 0 then y else x
    

    Dalam kasus sebelumnya, menerapkan pick '\0' 'x'memiliki tipe Char, sedangkan dalam kasus terakhir, semua yang Anda tahu tentang hasilnya adalah bahwa itu adalah Enum. (Ini juga alasan mengapa sebagian besar bahasa OO saat ini mengintegrasikan polimorfisme parametrik.)

  5. Terkait erat adalah masalah metode biner. Mereka sepenuhnya alami dengan kelas tipe:

    class Ord a where
      (<) :: a -> a -> Bool
      ...
    
    min :: Ord a => a -> a -> a
    min x y = if x < y then x else y
    

    Dengan subtyping saja, Ordantarmuka tidak mungkin untuk diungkapkan. Anda memerlukan bentuk yang lebih rumit, rekursif, atau polimorfisme parametrik yang disebut "kuantifikasi terikat-F" untuk melakukannya secara akurat. Bandingkan Java Comparabledan penggunaannya:

    interface Comparable<T> {
      int compareTo(T y);
    };
    
    <T extends Comparable<T>> T min(T x, T y) {
      if (x.compareTo(y) < 0)
        return x;
      else
        return y;
    }
    

Di sisi lain, antarmuka berbasis subtyping secara alami memungkinkan pembentukan koleksi heterogen, misalnya daftar tipe List<C>dapat berisi anggota yang memiliki berbagai subtipe C(walaupun tidak mungkin untuk memulihkan jenis persisnya, kecuali dengan menggunakan downcast). Untuk melakukan hal yang sama berdasarkan kelas tipe, Anda memerlukan tipe eksistensial sebagai fitur tambahan.

Andreas Rossberg
sumber
Ah, itu masuk akal. Pengiriman berbasis tipe vs nilai mungkin adalah hal besar yang tidak saya pikirkan dengan baik. Masalah polimorfisme parametrik dan pengetikan yang lebih spesifik masuk akal. Saya baru saja menarik itu dan antarmuka berbasis subtyping bersama dalam pikiran saya (tampaknya saya pikir di Jawa: - /).
oconnor0
Apakah tipe-tipe eksistensial adalah sesuatu yang mirip dengan menciptakan subtipe Ctanpa kehadiran downcast?
oconnor0
Agak. Mereka adalah sarana untuk membuat tipe abstrak, yaitu menyembunyikan perwakilannya. Di Haskell, jika Anda juga melampirkan batasan kelas untuk itu, Anda masih bisa menggunakan metode kelas-kelas di atasnya, tetapi tidak ada yang lain. - Downcast sebenarnya adalah fitur yang terpisah dari subtyping dan kuantifikasi eksistensial, dan pada prinsipnya dapat ditambahkan di hadapan yang terakhir. Sama seperti ada bahasa OO yang tidak menyediakannya.
Andreas Rossberg
PS: FWIW, tipe wildcard di Jawa adalah tipe eksistensial, meskipun agak terbatas dan ad-hoc (yang mungkin menjadi bagian dari alasan mengapa mereka agak membingungkan).
Andreas Rossberg
1
@dierier, itu akan dibatasi untuk kasus-kasus yang dapat sepenuhnya diselesaikan secara statis. Selain itu, untuk mencocokkan kelas jenis itu akan memerlukan bentuk resolusi kelebihan beban yang mampu membedakan berdasarkan pada jenis kembali saja (lihat item 1).
Andreas Rossberg
6

Selain jawaban Andreas yang sangat baik, harap diingat bahwa kelas tipe dimaksudkan untuk merampingkan kelebihan beban , yang memengaruhi ruang nama global. Tidak ada kelebihan muatan di Haskell selain dari apa yang dapat Anda peroleh melalui kelas tipe. Sebaliknya, ketika Anda menggunakan antarmuka objek, hanya fungsi-fungsi yang dideklarasikan untuk mengambil argumen dari antarmuka itu yang perlu khawatir tentang nama fungsi di antarmuka itu. Jadi, antarmuka menyediakan ruang nama lokal.

Misalnya, Anda memiliki fmapantarmuka objek yang disebut "Functor". Akan sangat baik untuk memiliki yang lain fmapdi antarmuka lain, katakanlah "Structor". Setiap objek (atau kelas) dapat memilih dan memilih antarmuka mana yang ingin diimplementasikan. Sebaliknya, di Haskell, Anda hanya dapat memiliki satu di fmapdalam konteks tertentu. Anda tidak dapat mengimpor kelas tipe Functor dan Structor ke konteks yang sama.

Antarmuka objek lebih mirip dengan tanda tangan ML Standar daripada tipe kelas.

Uday Reddy
sumber
namun tampaknya ada hubungan yang erat antara modul ML dan kelas tipe Haskell. cse.unsw.edu.au/~chak/papers/DHC07.html
Steven Shaw
1

Dalam contoh konkret Anda (dengan kelas jenis Functor), implementasi Haskell dan Java berperilaku berbeda. Bayangkan Anda memiliki tipe data Mungkin dan ingin menjadi Functor (itu benar-benar tipe data populer di Haskell, yang dapat Anda implementasikan dengan mudah di Java juga). Dalam contoh Java Anda, Anda akan membuat Mungkin kelas mengimplementasikan antarmuka Functor Anda. Jadi, Anda dapat menulis yang berikut ini (hanya kode semu karena saya memiliki latar belakang c #):

Maybe<Int> val = new Maybe<Int>(5);
Functor<Int> res = val.fmap(someFunctionHere);

Perhatikan bahwa resmemiliki tipe Functor, bukan Maybe. Jadi ini membuat implementasi Java hampir tidak dapat digunakan karena Anda kehilangan informasi jenis konkret, dan perlu melakukan gips. (setidaknya saya gagal menulis implementasi seperti itu di mana jenis masih ada). Dengan kelas tipe Haskell Anda akan mendapatkan Maybe Int sebagai hasilnya.

struhtanov
sumber
Saya pikir masalah ini adalah karena Java tidak mendukung jenis yang lebih tinggi, dan tidak terkait dengan diskusi typeclasses Vs antarmuka. Jika Java memiliki jenis yang lebih tinggi, maka fmap dapat mengembalikan aMaybe<Int> .
dcastro