Begini cara Haskell melakukannya: (tidak persis berlawanan dengan pernyataan Lippert karena Haskell bukan bahasa Berorientasi Objek).
PERINGATAN: jawaban panjang lebar dari penggemar Haskell yang serius di depan.
TL; DR
Contoh ini menggambarkan secara persis perbedaan Haskell dari C #. Alih-alih mendelegasikan logistik konstruksi struktur ke konstruktor, itu harus ditangani dalam kode di sekitarnya. Tidak ada cara untuk Nothingnilai nilai null (Atau dalam Haskell) untuk muncul di mana kami mengharapkan nilai non-nol karena nilai nol hanya dapat terjadi dalam jenis pembungkus khusus Maybeyang disebut yang tidak dapat dipertukarkan dengan / langsung dapat dikonversi menjadi, tidak langsung jenis yang dapat dibatalkan. Untuk menggunakan nilai yang dibuat nullable dengan membungkusnya dalam Maybe, kita harus terlebih dahulu mengekstraksi nilai menggunakan pencocokan pola, yang memaksa kita untuk mengalihkan aliran kontrol ke cabang di mana kita tahu pasti bahwa kita memiliki nilai non-nol.
Karena itu:
dapatkah kita selalu tahu bahwa referensi yang tidak dapat dibatalkan tidak pernah dalam keadaan apapun dianggap tidak valid?
Iya. Intdan Maybe Intdua tipe yang sepenuhnya terpisah. Menemukan Nothingdi dataran Intakan sebanding dengan menemukan "ikan" string dalam Int32.
Bagaimana dengan konstruktor objek dengan bidang tipe referensi yang tidak dapat dibatalkan?
Bukan masalah: konstruktor nilai di Haskell tidak bisa melakukan apa pun selain mengambil nilai yang diberikan dan menyatukannya. Semua logika inisialisasi terjadi sebelum konstruktor dipanggil.
Bagaimana dengan di finalizer dari objek seperti itu, di mana objek tersebut selesai karena kode yang seharusnya mengisi referensi melemparkan pengecualian?
Tidak ada finalizer di Haskell, jadi saya tidak bisa mengatasi ini. Namun, tanggapan pertama saya tetap bertahan.
Jawaban Lengkap :
Haskell tidak memiliki null dan menggunakan Maybetipe data untuk mewakili nullables. Mungkin tipe data aljabar didefinisikan seperti ini:
data Maybe a = Just a | Nothing
Bagi Anda yang tidak terbiasa dengan Haskell, baca ini sebagai "A Maybeadalah salah satu Nothingatau Just a". Secara khusus:
Maybeadalah konstruktor tipe : dapat dianggap (salah) sebagai kelas generik (di mana avariabel tipe). Analogi C # adalah class Maybe<a>{}.
Justadalah konstruktor nilai : ini adalah fungsi yang mengambil satu argumen tipe adan mengembalikan nilai tipe Maybe ayang berisi nilai. Jadi kodenya x = Just 17analog dengan int? x = 17;.
Nothingadalah konstruktor nilai lain, tetapi tidak memerlukan argumen dan yang Maybedikembalikan tidak memiliki nilai selain "Tidak Ada". x = Nothinganalog dengan int? x = null;(dengan asumsi kita membatasi aHaskell kita Int, yang dapat dilakukan dengan menulis x = Nothing :: Maybe Int).
Sekarang setelah dasar-dasar dari Maybetipe tersebut keluar dari jalan, bagaimana Haskell menghindari masalah yang dibahas dalam pertanyaan OP?
Yah, Haskell benar - benar berbeda dari sebagian besar bahasa yang dibahas sejauh ini, jadi saya akan mulai dengan menjelaskan beberapa prinsip dasar bahasa.
Pertama, di Haskell, semuanya tidak berubah . Segala sesuatu. Nama merujuk ke nilai, bukan ke lokasi memori tempat nilai dapat disimpan (ini saja merupakan sumber penghapusan bug yang sangat besar). Tidak seperti di C #, di mana deklarasi variabel dan tugas adalah dua operasi terpisah, di Haskell nilai diciptakan dengan mendefinisikan nilai mereka (misalnya x = 15, y = "quux", z = Nothing), yang tidak pernah bisa berubah. Karenanya, kode seperti:
ReferenceType x;
Tidak mungkin di Haskell. Tidak ada masalah dengan menginisialisasi nilai nullkarena semuanya harus secara eksplisit diinisialisasi ke nilai agar ada.
Kedua, Haskell bukan bahasa berorientasi objek : itu adalah bahasa murni fungsional , jadi tidak ada objek dalam arti kata yang ketat. Sebaliknya, ada hanya fungsi (konstruktor nilai) yang mengambil argumen mereka dan mengembalikan struktur yang digabung.
Selanjutnya, sama sekali tidak ada kode gaya imperatif. Maksud saya, sebagian besar bahasa mengikuti pola seperti ini:
do thing 1
add thing 2 to thing 3
do thing 4
if thing 5:
do thing 6
return thing 7
Perilaku program dinyatakan sebagai serangkaian instruksi. Dalam bahasa yang Berorientasi Objek, deklarasi kelas dan fungsi juga memainkan peran besar dalam aliran program, tetapi pada intinya, "daging" dari eksekusi program mengambil bentuk serangkaian instruksi yang akan dieksekusi.
Di Haskell, ini tidak mungkin. Alih-alih, aliran program ditentukan sepenuhnya oleh fungsi chaining. Bahkan donotasi yang tampak imperatif hanyalah gula sintaksis untuk meneruskan fungsi anonim kepada >>=operator. Semua fungsi berbentuk:
<optional explicit type signature>
functionName arg1 arg2 ... argn = body-expression
Di mana body-expressionbisa apa saja yang mengevaluasi suatu nilai. Jelas ada lebih banyak fitur sintaksis yang tersedia tetapi intinya adalah tidak adanya urutan pernyataan yang lengkap.
Terakhir, dan mungkin yang paling penting, sistem tipe Haskell sangat ketat. Jika saya harus meringkas filosofi desain pusat dari sistem tipe Haskell, saya akan mengatakan: "Buat sebanyak mungkin hal yang salah pada waktu kompilasi sehingga sesedikit mungkin menjadi salah saat runtime." Tidak ada konversi tersirat apa pun (ingin mempromosikan Intke Double? Gunakan fromIntegralfungsi). Satu-satunya yang mungkin memiliki nilai tidak valid terjadi pada saat runtime adalah menggunakan Prelude.undefined(yang tampaknya hanya harus ada di sana dan tidak mungkin untuk menghapus ).
Dengan semua ini dalam pikiran, mari kita lihat contoh "rusak" amon dan coba untuk mengekspresikan kembali kode ini di Haskell. Pertama, deklarasi data (menggunakan sintaks rekaman untuk bidang bernama):
data NotSoBroken = NotSoBroken {foo :: Foo, bar :: Bar }
( foodan barbenar-benar fungsi accessor ke bidang anonim di sini alih-alih bidang yang sebenarnya, tetapi kita dapat mengabaikan detail ini).
The NotSoBrokennilai konstruktor tidak mampu mengambil tindakan apapun selain mengambil Foodan Bar(yang tidak nullable) dan membuat NotSoBrokenkeluar dari mereka. Tidak ada tempat untuk meletakkan kode imperatif atau bahkan secara manual menetapkan bidang. Semua logika inisialisasi harus dilakukan di tempat lain, kemungkinan besar dalam fungsi pabrik khusus.
Dalam contohnya, konstruksi Brokenselalu gagal. Tidak ada cara untuk mematahkan NotSoBrokenkonstruktor nilai dengan cara yang sama (tidak ada tempat untuk menulis kode), tetapi kita dapat membuat fungsi pabrik yang sama-sama cacat.
makeNotSoBroken :: Foo -> Bar -> Maybe NotSoBroken
makeNotSoBroken foo bar = Nothing
(baris pertama adalah tipe deklarasi tanda tangan: makeNotSoBrokenmengambil argumen a Foodan a Barsebagai dan menghasilkan a Maybe NotSoBroken).
Tipe pengembalian harus Maybe NotSoBrokendan bukan hanya NotSoBrokenkarena kami menyuruhnya untuk mengevaluasi Nothing, yang merupakan konstruktor nilai untuk Maybe. Jenis tidak akan berbaris jika kita menulis sesuatu yang berbeda.
Selain tidak ada gunanya, fungsi ini bahkan tidak memenuhi tujuan sebenarnya, seperti yang akan kita lihat ketika kita mencoba menggunakannya. Mari kita membuat fungsi yang disebut useNotSoBrokenyang mengharapkan NotSoBrokensebagai argumen:
useNotSoBroken :: NotSoBroken -> Whatever
( useNotSoBrokenmenerima a NotSoBrokensebagai argumen dan menghasilkan a Whatever).
Dan gunakan seperti ini:
useNotSoBroken (makeNotSoBroken)
Dalam sebagian besar bahasa, perilaku semacam ini dapat menyebabkan pengecualian penunjuk nol. Di Haskell, tipe tidak cocok: makeNotSoBrokenmengembalikan a Maybe NotSoBroken, tetapi useNotSoBrokenmengharapkan a NotSoBroken. Jenis ini tidak dapat dipertukarkan, dan kode gagal dikompilasi.
Untuk menyiasatinya, kita bisa menggunakan casepernyataan untuk bercabang berdasarkan pada struktur Maybenilai (menggunakan fitur yang disebut pencocokan pola ):
case makeNotSoBroken of
Nothing -> --handle situation here
(Just x) -> useNotSoBroken x
Jelas potongan ini perlu ditempatkan di dalam beberapa konteks untuk benar-benar dikompilasi, tetapi ini menunjukkan dasar-dasar bagaimana Haskell menangani nullables. Berikut ini penjelasan langkah demi langkah dari kode di atas:
- Pertama,
makeNotSoBrokendievaluasi, yang dijamin menghasilkan nilai tipe Maybe NotSoBroken.
- The
casepernyataan memeriksa struktur nilai ini.
- Jika nilainya
Nothing, kode "pegangan situasi di sini" dievaluasi.
- Jika nilainya cocok dengan
Justnilai, cabang lainnya dieksekusi. Perhatikan bagaimana klausa yang cocok secara bersamaan mengidentifikasi nilai sebagai Justkonstruksi dan mengikat NotSoBrokenbidang internalnya ke sebuah nama (dalam hal ini, x). xkemudian dapat digunakan seperti nilai normal NotSoBrokenitu.
Jadi, pencocokan pola menyediakan fasilitas yang kuat untuk menegakkan keamanan jenis, karena struktur objek tidak dapat dipisahkan dengan percabangan kontrol.
Saya harap ini adalah penjelasan yang komprehensif. Jika itu tidak masuk akal, lompatlah ke Learn You A Haskell For Great Good! , salah satu tutorial bahasa online terbaik yang pernah saya baca. Semoga Anda akan melihat keindahan yang sama dalam bahasa ini yang saya lakukan.