Apakah monads merupakan alternatif (mungkin lebih disukai) dari hierarki warisan?

20

Saya akan menggunakan deskripsi bahasa-agnostik dari monad seperti ini, pertama menggambarkan monoid:

Sebuah monoid adalah (kira-kira) satu set fungsi yang mengambil beberapa jenis sebagai parameter dan mengembalikan tipe yang sama.

Sebuah monad adalah (kira-kira) satu set fungsi yang mengambil pembungkus tipe sebagai parameter dan mengembalikan tipe pembungkus yang sama.

Perhatikan itu adalah deskripsi, bukan definisi. Jangan ragu untuk menyerang deskripsi itu!

Jadi dalam bahasa OO, monad mengizinkan komposisi operasi seperti:

Flier<Duck> m = new Flier<Duck>(duck).takeOff().flyAround().land()

Perhatikan bahwa monad mendefinisikan dan mengontrol semantik operasi tersebut, bukan kelas yang terkandung.

Secara tradisional, dalam bahasa OO kami akan menggunakan hierarki kelas & warisan untuk menyediakan semantik tersebut. Jadi kami akan memiliki Birdkelas dengan metode takeOff(), flyAround()dan land(), dan Bebek akan mewarisi itu.

Tapi kemudian kita mendapat masalah dengan burung yang tidak bisa terbang, karena penguin.takeOff()gagal. Kita harus menggunakan lemparan dan penanganan Exception.

Juga, setelah kami mengatakan bahwa Penguin adalah Bird, kami mengalami masalah dengan banyak pewarisan, misalnya jika kami juga memiliki hierarki Swimmer.

Pada dasarnya kami mencoba untuk menempatkan kelas ke dalam kategori (dengan permintaan maaf kepada Teori Kategori guys), dan mendefinisikan semantik berdasarkan kategori daripada di kelas individu. Tetapi, monad tampaknya merupakan mekanisme yang jauh lebih jelas untuk melakukan hal itu daripada hierarki.

Jadi dalam hal ini, kita akan memiliki Flier<T>monad seperti contoh di atas:

Flier<Duck> m = new Flier<Duck>(duck).takeOff().flyAround().land()

... dan kami tidak akan pernah instantiate a Flier<Penguin>. Kami bahkan dapat menggunakan pengetikan statis untuk mencegah hal itu terjadi, mungkin dengan antarmuka penanda. Atau pengecekan kemampuan runtime untuk menyelamatkan. Tapi sungguh, seorang programmer seharusnya tidak pernah memasukkan Penguin ke Flier, dalam arti yang sama mereka tidak boleh membagi dengan nol.

Juga, ini lebih umum berlaku. Selebaran tidak harus menjadi Burung. Misalnya Flier<Pterodactyl>, atau Flier<Squirrel>, tanpa mengubah semantik dari masing-masing tipe tersebut.

Setelah kita mengklasifikasikan semantik berdasarkan fungsi komposable pada wadah - alih-alih dengan hierarki tipe - itu menyelesaikan masalah lama dengan kelas yang "semacam dilakukan, jenis tidak" cocok dengan hierarki tertentu. Ini juga dengan mudah & jelas memungkinkan beberapa semantik untuk suatu kelas, seperti Flier<Duck>juga Swimmer<Duck>. Sepertinya kita telah berjuang dengan ketidakcocokan impedansi dengan mengklasifikasikan perilaku dengan hierarki kelas. Monads menanganinya dengan elegan.

Jadi pertanyaan saya adalah, dengan cara yang sama ketika kita lebih menyukai komposisi daripada warisan, apakah masuk akal juga untuk memilih monad daripada warisan?

(BTW saya tidak yakin apakah ini harus ada di sini atau di Comp Sci, tetapi ini tampaknya lebih seperti masalah pemodelan praktis. Tapi mungkin lebih baik di sana.)

rampok
sumber
1
Tidak yakin saya mengerti cara kerjanya: tupai dan bebek tidak terbang dengan cara yang sama - jadi "aksi terbang" perlu diterapkan di kelas-kelas itu ... Dan brosur membutuhkan metode untuk membuat tupai dan bebek terbang ... Mungkin dalam antarmuka Flier yang umum ... Ups tunggu sebentar ... Apakah saya melewatkan sesuatu?
assylias
Antarmuka berbeda dari warisan kelas, karena antarmuka menentukan kemampuan sementara warisan fungsional menentukan perilaku aktual. Bahkan dalam "komposisi lebih dari warisan", mendefinisikan antarmuka masih merupakan mekanisme penting (misalnya polimorfisme). Antarmuka tidak mengalami masalah pewarisan berganda yang sama. Plus, masing-masing brosur dapat memberikan (melalui antarmuka & polimorfisme) sifat-sifat kemampuan seperti "getFlightSpeed ​​()" atau "getManuverability ()" untuk digunakan wadah.
Rob
3
Apakah Anda mencoba bertanya apakah menggunakan polimorfisme parametrik selalu menjadi alternatif yang layak untuk polimorfisme subtipe?
ChaosPandion
ya, dengan kerutan menambahkan fungsi komposable yang melestarikan semantik. Jenis kontainer berparameter sudah ada sejak lama, tetapi dengan sendirinya mereka tidak menganggap saya sebagai jawaban yang lengkap. Jadi itu sebabnya saya bertanya-tanya apakah pola monad memiliki peran yang lebih mendasar.
Rob
6
Saya tidak mengerti deskripsi Anda tentang monoids dan monad. Properti utama dari monoids adalah bahwa ia melibatkan operasi biner asosiatif (pikirkan penambahan floating-point, integer multiplication, atau string concatenation). Monad adalah abstraksi yang mendukung urutan berbagai (mungkin tergantung) perhitungan dalam beberapa urutan.
Rufflewind

Jawaban:

15

Jawaban singkatnya adalah tidak , monad bukan alternatif untuk hierarki warisan (juga dikenal sebagai subtipe polimorfisme). Anda tampaknya menggambarkan polimorfisme parametrik , yang digunakan monad tetapi bukan satu-satunya hal yang dapat dilakukan.

Sejauh yang saya mengerti, pada dasarnya monad tidak ada hubungannya dengan warisan. Saya akan mengatakan bahwa kedua hal ini lebih atau kurang orthogonal: mereka dimaksudkan untuk mengatasi masalah yang berbeda, dan karenanya:

  1. Mereka dapat digunakan secara sinergis setidaknya dalam dua pengertian:
    • lihat Typeclassopedia , yang mencakup banyak kelas tipe Haskell. Anda akan melihat bahwa ada hubungan seperti warisan di antara mereka. Misalnya, Monad adalah turunan dari Applicative yang sendiri merupakan turunan dari Functor.
    • tipe data yang merupakan contoh Monads dapat berpartisipasi dalam hierarki kelas. Ingat, Monad lebih seperti antarmuka - mengimplementasikannya untuk tipe tertentu memberi tahu Anda beberapa hal tentang tipe data, tetapi tidak semuanya.
  2. Mencoba menggunakan satu untuk melakukan yang lain akan sulit dan jelek.

Akhirnya, meskipun ini bersinggungan dengan pertanyaan Anda, Anda mungkin tertarik untuk mengetahui bahwa monad memang memiliki beberapa cara yang sangat kuat untuk menyusun; baca tentang transformer monad untuk mencari tahu lebih lanjut. Namun, ini masih merupakan area penelitian aktif karena kami (dan kami, yang saya maksud orang 100000x lebih pintar dari saya) belum menemukan cara yang bagus untuk membuat monad, dan sepertinya beberapa monad tidak menulis secara sewenang-wenang.


Sekarang untuk memilih pertanyaan Anda (maaf, saya bermaksud agar ini membantu, dan tidak membuat Anda merasa buruk): Saya merasa ada banyak tempat yang dipertanyakan yang akan saya coba jelaskan.

  1. Monad adalah sekumpulan fungsi yang menggunakan tipe kontainer sebagai parameter dan mengembalikan tipe kontainer yang sama.

    Tidak, ini ada Monaddi Haskell: tipe parameter m adengan implementasi return :: a -> m adan (>>=) :: m a -> (a -> m b) -> m b, memenuhi undang-undang berikut:

    return a >>= k  ==  k a
    m >>= return  ==  m
    m >>= (\x -> k x >>= h)  ==  (m >>= k) >>= h
    

    Ada beberapa contoh Monad yang bukan wadah ( (->) b), dan ada beberapa wadah yang bukan (dan tidak dapat dibuat) contoh dari Monad ( Set, karena batasan kelas tipe). Jadi intuisi "wadah" adalah yang buruk. Lihat ini untuk lebih banyak contoh.

  2. Jadi dalam bahasa OO, monad mengizinkan komposisi operasi seperti:

      Flier<Duck> m = new Flier<Duck>(duck).takeOff().flyAround().land()
    

    Tidak, tidak sama sekali. Contoh itu tidak memerlukan Monad. Yang diperlukan hanyalah fungsi dengan mencocokkan jenis input dan output. Berikut cara lain untuk menulisnya yang menekankan bahwa itu hanya aplikasi fungsi:

    Flier<Duck> m = land(flyAround(takeOff(new Flier<Duck>(duck))));
    

    Saya percaya ini adalah pola yang dikenal sebagai "antarmuka yang lancar" atau "metode chaining" (tapi saya tidak yakin).

  3. Perhatikan bahwa monad mendefinisikan dan mengontrol semantik operasi tersebut, bukan kelas yang terkandung.

    Tipe data yang juga monad dapat (dan hampir selalu melakukannya!) Memiliki operasi yang tidak terkait dengan monad. Berikut adalah contoh Haskell yang terdiri dari tiga fungsi []yang tidak ada hubungannya dengan monad: []"mendefinisikan dan mengontrol semantik operasi" dan "kelas yang terkandung" tidak, tetapi itu tidak cukup untuk membuat monad:

    \predicate -> length . filter predicate . reverse
    
  4. Anda telah mencatat dengan benar bahwa ada masalah dengan menggunakan hierarki kelas untuk memodelkan sesuatu. Namun, contoh Anda tidak menunjukkan bukti bahwa monad dapat:

    • Lakukan pekerjaan dengan baik pada hal-hal yang bisa dilakukan warisan
    • Lakukan pekerjaan dengan baik pada hal-hal yang warisannya buruk
Komunitas
sumber
3
Terima kasih! Banyak yang harus saya proses. Saya tidak merasa buruk - Saya sangat menghargai wawasan ini. Saya akan merasa lebih buruk membawa ide-ide buruk. :) (Pergi ke seluruh titik stackexchange!)
Rob
1
@Roby Sama-sama! Ngomong-ngomong, jika Anda belum pernah mendengarnya, saya merekomendasikan LYAH karena ini adalah sumber yang bagus untuk belajar monad (dan Haskell!) Karena memiliki banyak contoh (dan saya merasa bahwa melakukan banyak contoh adalah cara terbaik untuk mengatasi monads).
Ada banyak di sini; Saya tidak ingin membanjiri komentar, tetapi beberapa komentar: # 2 land(flyAround(takeOff(new Flier<Duck>(duck))))tidak bekerja (setidaknya dalam OO) karena konstruksi itu membutuhkan pemecahan enkapsulasi untuk mendapatkan rincian Flier. Dengan merantai opertions di kelas, detail Flier tetap tersembunyi, dan itu dapat mempertahankan semantiknya. Itu mirip dengan alasan bahwa di Haskell sebuah monad mengikat (a, M b)dan tidak (M a, M b)sehingga monad tidak harus mengekspos keadaannya ke fungsi "aksi".
Rob
# 1, sayangnya saya mencoba mengaburkan definisi ketat Monad di Haskell, karena memetakan apa pun ke Haskell memiliki masalah besar: komposisi fungsi, termasuk komposisi pada konstruktor , yang tidak dapat Anda lakukan dengan mudah dalam bahasa pejalan kaki seperti Jawa. Jadi unitmenjadi (kebanyakan) konstruktor pada tipe yang terkandung, dan bindmenjadi (sebagian besar) operasi waktu kompilasi tersirat (yaitu mengikat awal) yang mengikat fungsi "aksi" ke kelas. Jika Anda memiliki fungsi kelas satu, atau kelas Function <A, Monad <B>>, maka bindmetode dapat melakukan pengikatan terlambat, tapi saya akan mengambil penyalahgunaan itu berikutnya. ;)
Rob
# 3 setuju, dan itulah keindahannya. Jika Flier<Thing>mengontrol semantik penerbangan, maka itu dapat memaparkan banyak data dan operasi yang memelihara semantik penerbangan, sementara semantik khusus "monad" benar-benar hanya tentang membuatnya semuat rantai dan dirangkum. Kekhawatiran itu mungkin tidak (dan dengan yang saya gunakan, tidak) menyangkut kelas di dalam monad: misalnya Resource<String>memiliki properti httpStatus, tetapi String tidak.
Rob
1

Jadi pertanyaan saya adalah, dengan cara yang sama ketika kita lebih menyukai komposisi daripada warisan, apakah masuk akal juga untuk memilih monad daripada warisan?

Dalam bahasa non-OO, ya. Dalam bahasa OO yang lebih tradisional, saya akan mengatakan tidak.

Masalahnya adalah bahwa sebagian besar bahasa tidak memiliki spesialisasi jenis, artinya Anda tidak dapat membuat Flier<Squirrel>dan Flier<Bird>memiliki implementasi yang berbeda. Anda harus melakukan sesuatu seperti static Flier Flier::Create(Squirrel)(dan kemudian kelebihan untuk setiap jenis). Yang pada gilirannya berarti Anda harus memodifikasi jenis ini setiap kali Anda menambahkan hewan baru, dan kemungkinan menduplikat sedikit kode untuk membuatnya berfungsi.

Oh, dan Dalam tidak sedikit bahasa (C # misalnya) public class Flier<T> : T {}adalah ilegal. Itu bahkan tidak akan membangun. Sebagian besar, jika tidak semua programmer OO berharap Flier<Bird>masih menjadi Bird.

Telastyn
sumber
terima kasih atas komentarnya. Saya punya beberapa pemikiran lagi, tetapi hanya sepele, meskipun Flier<Bird>merupakan wadah parameter, tidak ada yang akan menganggapnya sebagai Bird(!?) List<String>Adalah Daftar bukan String.
Rob
@RobY - Flierbukan hanya sebuah wadah. Jika Anda menganggapnya hanya sebagai wadah, mengapa Anda pernah berpikir bahwa itu bisa menggantikan penggunaan warisan?
Telastyn
Saya kehilangan Anda di sana ... maksud saya adalah monad adalah wadah yang ditingkatkan. Animal / Bird / Penguinbiasanya merupakan contoh buruk, karena membawa semua jenis semantik. Contoh praktisnya adalah REST-ish monad yang kami gunakan: Resource<String>.from(uri).get() Resourcemenambahkan semantik di atas String(atau tipe lain), jadi jelas bukan String.
Rob
@RobY - tapi kemudian sama sekali tidak terkait dengan warisan.
Telastyn
Kecuali bahwa itu jenis penahanan yang berbeda. Saya bisa meletakkan String ke Resource, atau saya bisa abstrak kelas ResourceString dan menggunakan warisan. Pikiran saya adalah bahwa menempatkan kelas dalam wadah rantai adalah cara yang lebih baik untuk perilaku abstrak daripada memasukkannya ke dalam hierarki kelas dengan warisan. Jadi "tidak ada cara terkait" dalam arti "mengganti / meniadakan" - ya.
Rob