Đây là cách Haskell thực hiện: (không chính xác là đối trọng với các tuyên bố của Lippert vì Haskell không phải là ngôn ngữ hướng đối tượng).
CẢNH BÁO: câu trả lời dài dòng từ một fanboy Haskell nghiêm túc phía trước.
TL; DR
Ví dụ này minh họa chính xác Haskell khác với C # như thế nào. Thay vì ủy thác hậu cần xây dựng kết cấu cho một nhà xây dựng, nó phải được xử lý theo mã xung quanh. Không có cách nào để giá trị null (Hoặc Nothing
trong Haskell) tăng lên khi chúng ta đang mong đợi một giá trị khác null vì các giá trị null chỉ có thể xảy ra trong các loại trình bao bọc đặc biệt được gọi Maybe
là không thể hoán đổi với / chuyển đổi trực tiếp thành thông thường, không các loại nullable. Để sử dụng một giá trị không thể thực hiện được bằng cách gói nó vào một Maybe
, trước tiên chúng ta phải trích xuất giá trị bằng cách sử dụng khớp mẫu, điều này buộc chúng ta phải chuyển hướng điều khiển vào một nhánh nơi chúng ta biết chắc chắn rằng chúng ta có giá trị không null.
Vì thế:
chúng ta có thể luôn biết rằng một tài liệu tham khảo không có giá trị không bao giờ trong bất kỳ trường hợp nào được quan sát là không hợp lệ?
Đúng. Int
và Maybe Int
là hai loại hoàn toàn riêng biệt. Tìm Nothing
trong một đồng bằng Int
sẽ tương đương với việc tìm chuỗi "cá" trong một Int32
.
Điều gì về hàm tạo của một đối tượng với trường tham chiếu không thể rỗng của kiểu tham chiếu?
Không thành vấn đề: các nhà xây dựng giá trị trong Haskell không thể làm gì ngoài việc lấy các giá trị họ đưa ra và đặt chúng lại với nhau. Tất cả logic khởi tạo diễn ra trước khi hàm tạo được gọi.
Thế còn trong phần hoàn thiện của một đối tượng như vậy, trong đó đối tượng được hoàn thành bởi vì mã được cho là điền vào tham chiếu đã ném một ngoại lệ?
Không có người hoàn thiện trong Haskell, vì vậy tôi thực sự không thể giải quyết điều này. Phản ứng đầu tiên của tôi vẫn đứng, tuy nhiên.
Trả lời đầy đủ :
Haskell không có null và sử dụng Maybe
kiểu dữ liệu để biểu thị nullables. Có thể là một kiểu dữ liệu đại số được định nghĩa như thế này:
data Maybe a = Just a | Nothing
Đối với những người không quen thuộc với Haskell, hãy đọc phần này là "A Maybe
là a Nothing
hoặc a Just a
". Đặc biệt:
Maybe
là hàm tạo kiểu : có thể nghĩ (không chính xác) là một lớp chung (trong đó a
là biến kiểu). Sự tương tự C # là class Maybe<a>{}
.
Just
là một hàm tạo giá trị : nó là một hàm lấy một đối số của kiểu a
và trả về một giá trị của kiểu Maybe a
chứa giá trị. Vì vậy, mã x = Just 17
tương tự như int? x = 17;
.
Nothing
là một hàm tạo giá trị khác, nhưng nó không có đối số và Maybe
trả về không có giá trị nào ngoài "Không có gì". x = Nothing
tương tự như int? x = null;
(giả sử chúng ta hạn chế a
trong Haskell Int
, điều này có thể được thực hiện bằng cách viết x = Nothing :: Maybe Int
).
Bây giờ, những điều cơ bản của Maybe
loại này đã được đưa ra, làm thế nào để Haskell tránh được các vấn đề được thảo luận trong câu hỏi của OP?
Chà, Haskell thực sự khác biệt với hầu hết các ngôn ngữ được thảo luận cho đến nay, vì vậy tôi sẽ bắt đầu bằng cách giải thích một vài nguyên tắc ngôn ngữ cơ bản.
Trước hết, ở Haskell, mọi thứ đều bất biến . Mọi điều. Các tên đề cập đến các giá trị, không phải các vị trí bộ nhớ nơi các giá trị có thể được lưu trữ (chỉ riêng điều này là một nguồn loại bỏ lỗi rất lớn). Không giống như trong C #, nơi biến khai và phân công là hai hoạt động riêng biệt, trong các giá trị Haskell được tạo ra bằng cách định nghĩa giá trị của họ (ví dụ như x = 15
, y = "quux"
, z = Nothing
), mà không bao giờ có thể thay đổi. Do đó, mã như:
ReferenceType x;
Không thể có trong Haskell. Không có vấn đề với việc khởi tạo giá trị null
bởi vì mọi thứ phải được khởi tạo rõ ràng thành một giá trị để nó tồn tại.
Thứ hai, Haskell không phải là một ngôn ngữ hướng đối tượng : nó là một ngôn ngữ chức năng thuần túy , vì vậy không có đối tượng theo nghĩa chặt chẽ của từ này. Thay vào đó, có các hàm đơn giản (các hàm tạo giá trị) lấy các đối số của chúng và trả về một cấu trúc hợp nhất.
Tiếp theo, hoàn toàn không có mã kiểu bắt buộc. Bằng cách này, tôi có nghĩa là hầu hết các ngôn ngữ theo một mô hình như thế này:
do thing 1
add thing 2 to thing 3
do thing 4
if thing 5:
do thing 6
return thing 7
Hành vi chương trình được thể hiện như một loạt các hướng dẫn. Trong các ngôn ngữ hướng đối tượng, khai báo lớp và hàm cũng đóng một vai trò rất lớn trong luồng chương trình, nhưng về bản chất, "thịt" của việc thực hiện chương trình có dạng một loạt các lệnh được thực thi.
Trong Haskell, điều này là không thể. Thay vào đó, luồng chương trình được quyết định hoàn toàn bởi các hàm chuỗi. Ngay cả chú thích bắt buộc do
cũng chỉ là đường cú pháp để truyền các hàm ẩn danh cho >>=
toán tử. Tất cả các chức năng có dạng:
<optional explicit type signature>
functionName arg1 arg2 ... argn = body-expression
Nơi body-expression
có thể là bất cứ điều gì đánh giá một giá trị. Rõ ràng là có nhiều tính năng cú pháp có sẵn nhưng điểm chính là sự vắng mặt hoàn toàn của chuỗi các câu lệnh.
Cuối cùng, và có lẽ là quan trọng nhất, hệ thống loại của Haskell cực kỳ nghiêm ngặt. Nếu tôi phải tóm tắt triết lý thiết kế trung tâm của hệ thống loại Haskell, tôi sẽ nói: "Làm cho càng nhiều thứ càng tốt sai trong thời gian biên dịch sao cho càng ít càng tốt khi chạy sai." Không có chuyển đổi ngầm định nào (muốn quảng cáo Int
đến một Double
? Sử dụng fromIntegral
chức năng). Điều duy nhất có thể có một giá trị không hợp lệ xảy ra trong thời gian chạy là sử dụng Prelude.undefined
(dường như chỉ cần có ở đó và không thể xóa ).
Với tất cả những điều này, hãy xem ví dụ "bị hỏng" của amon và cố gắng diễn đạt lại mã này trong Haskell. Đầu tiên, khai báo dữ liệu (sử dụng cú pháp bản ghi cho các trường được đặt tên):
data NotSoBroken = NotSoBroken {foo :: Foo, bar :: Bar }
( foo
và bar
thực sự là các hàm truy cập vào các trường ẩn danh ở đây thay vì các trường thực tế, nhưng chúng ta có thể bỏ qua chi tiết này).
Hàm tạo NotSoBroken
giá trị không có khả năng thực hiện bất kỳ hành động nào ngoài việc thực hiện a Foo
và a Bar
(không thể rỗng) và tạo NotSoBroken
ra chúng. Không có nơi nào để đặt mã mệnh lệnh hoặc thậm chí gán các trường theo cách thủ công. Tất cả logic khởi tạo phải diễn ra ở nơi khác, rất có thể là trong một chức năng nhà máy chuyên dụng.
Trong ví dụ, việc xây dựng Broken
luôn luôn thất bại. Không có cách nào để phá vỡ hàm tạo NotSoBroken
giá trị theo cách tương tự (đơn giản là không có nơi nào để viết mã), nhưng chúng ta có thể tạo một hàm xuất xưởng bị lỗi tương tự.
makeNotSoBroken :: Foo -> Bar -> Maybe NotSoBroken
makeNotSoBroken foo bar = Nothing
(dòng đầu tiên là một khai báo chữ ký loại: makeNotSoBroken
lấy a Foo
và a Bar
làm đối số và tạo ra a Maybe NotSoBroken
).
Kiểu trả về phải Maybe NotSoBroken
và không chỉ đơn giản NotSoBroken
vì chúng tôi đã bảo nó đánh giá Nothing
, đó là một hàm tạo giá trị cho Maybe
. Các loại chỉ đơn giản là không xếp hàng nếu chúng tôi viết bất cứ điều gì khác nhau.
Ngoài việc hoàn toàn vô nghĩa, chức năng này thậm chí còn không hoàn thành mục đích thực sự của nó, như chúng ta sẽ thấy khi chúng ta cố gắng sử dụng nó. Hãy tạo một hàm gọi useNotSoBroken
mà hy vọng một NotSoBroken
như một cuộc tranh cãi:
useNotSoBroken :: NotSoBroken -> Whatever
( useNotSoBroken
chấp nhận một NotSoBroken
đối số và tạo ra một Whatever
).
Và sử dụng nó như vậy:
useNotSoBroken (makeNotSoBroken)
Trong hầu hết các ngôn ngữ, loại hành vi này có thể gây ra ngoại lệ con trỏ null. Trong Haskell, các loại không khớp với nhau: makeNotSoBroken
trả về a Maybe NotSoBroken
, nhưng useNotSoBroken
mong đợi a NotSoBroken
. Các loại này không thể thay thế cho nhau và mã không thể biên dịch.
Để giải quyết vấn đề này, chúng ta có thể sử dụng một case
câu lệnh để phân nhánh dựa trên cấu trúc của Maybe
giá trị (sử dụng một tính năng được gọi là khớp mẫu ):
case makeNotSoBroken of
Nothing -> --handle situation here
(Just x) -> useNotSoBroken x
Rõ ràng đoạn trích này cần được đặt bên trong một số bối cảnh để thực sự biên dịch, nhưng nó cho thấy những điều cơ bản về cách Haskell xử lý nullables. Dưới đây là giải thích từng bước của đoạn mã trên:
- Đầu tiên,
makeNotSoBroken
được đánh giá, được đảm bảo để tạo ra một giá trị của loại Maybe NotSoBroken
.
- Các
case
tuyên bố kiểm tra cấu trúc của giá trị này.
- Nếu giá trị là
Nothing
, mã "xử lý tình huống ở đây" được ước tính.
- Nếu giá trị thay vào đó khớp với một
Just
giá trị, nhánh khác được thực thi. Lưu ý cách mệnh đề khớp đồng thời xác định giá trị là một Just
cấu trúc và liên kết NotSoBroken
trường bên trong của nó với một tên (trong trường hợp này x
). x
sau đó có thể được sử dụng như NotSoBroken
giá trị bình thường .
Vì vậy, khớp mẫu cung cấp một phương tiện mạnh mẽ để thực thi an toàn kiểu, vì cấu trúc của đối tượng gắn liền với sự phân nhánh của điều khiển.
Tôi hy vọng đây là một lời giải thích dễ hiểu. Nếu nó không có ý nghĩa, hãy nhảy vào Learn You A Haskell For Great Good! , một trong những hướng dẫn ngôn ngữ trực tuyến tốt nhất mà tôi từng đọc. Hy vọng bạn sẽ thấy vẻ đẹp tương tự trong ngôn ngữ này mà tôi làm.