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 Nothing
nilai nilai null (Atau dalam Haskell) untuk muncul di mana kami mengharapkan nilai non-nol karena nilai nol hanya dapat terjadi dalam jenis pembungkus khusus Maybe
yang 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. Int
dan Maybe Int
dua tipe yang sepenuhnya terpisah. Menemukan Nothing
di dataran Int
akan 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 Maybe
tipe 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 Maybe
adalah salah satu Nothing
atau Just a
". Secara khusus:
Maybe
adalah konstruktor tipe : dapat dianggap (salah) sebagai kelas generik (di mana a
variabel tipe). Analogi C # adalah class Maybe<a>{}
.
Just
adalah konstruktor nilai : ini adalah fungsi yang mengambil satu argumen tipe a
dan mengembalikan nilai tipe Maybe a
yang berisi nilai. Jadi kodenya x = Just 17
analog dengan int? x = 17;
.
Nothing
adalah konstruktor nilai lain, tetapi tidak memerlukan argumen dan yang Maybe
dikembalikan tidak memiliki nilai selain "Tidak Ada". x = Nothing
analog dengan int? x = null;
(dengan asumsi kita membatasi a
Haskell kita Int
, yang dapat dilakukan dengan menulis x = Nothing :: Maybe Int
).
Sekarang setelah dasar-dasar dari Maybe
tipe 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 null
karena 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 do
notasi 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-expression
bisa 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 Int
ke Double
? Gunakan fromIntegral
fungsi). 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 }
( foo
dan bar
benar-benar fungsi accessor ke bidang anonim di sini alih-alih bidang yang sebenarnya, tetapi kita dapat mengabaikan detail ini).
The NotSoBroken
nilai konstruktor tidak mampu mengambil tindakan apapun selain mengambil Foo
dan Bar
(yang tidak nullable) dan membuat NotSoBroken
keluar 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 Broken
selalu gagal. Tidak ada cara untuk mematahkan NotSoBroken
konstruktor 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: makeNotSoBroken
mengambil argumen a Foo
dan a Bar
sebagai dan menghasilkan a Maybe NotSoBroken
).
Tipe pengembalian harus Maybe NotSoBroken
dan bukan hanya NotSoBroken
karena 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 useNotSoBroken
yang mengharapkan NotSoBroken
sebagai argumen:
useNotSoBroken :: NotSoBroken -> Whatever
( useNotSoBroken
menerima a NotSoBroken
sebagai 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: makeNotSoBroken
mengembalikan a Maybe NotSoBroken
, tetapi useNotSoBroken
mengharapkan a NotSoBroken
. Jenis ini tidak dapat dipertukarkan, dan kode gagal dikompilasi.
Untuk menyiasatinya, kita bisa menggunakan case
pernyataan untuk bercabang berdasarkan pada struktur Maybe
nilai (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,
makeNotSoBroken
dievaluasi, yang dijamin menghasilkan nilai tipe Maybe NotSoBroken
.
- The
case
pernyataan memeriksa struktur nilai ini.
- Jika nilainya
Nothing
, kode "pegangan situasi di sini" dievaluasi.
- Jika nilainya cocok dengan
Just
nilai, cabang lainnya dieksekusi. Perhatikan bagaimana klausa yang cocok secara bersamaan mengidentifikasi nilai sebagai Just
konstruksi dan mengikat NotSoBroken
bidang internalnya ke sebuah nama (dalam hal ini, x
). x
kemudian dapat digunakan seperti nilai normal NotSoBroken
itu.
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.