Đã một năm kể từ khi tôi đăng câu hỏi này. Sau khi đăng nó, tôi đào sâu vào Haskell trong một vài tháng. Tôi rất thích nó, nhưng tôi đã đặt nó sang một bên ngay khi tôi sẵn sàng đi sâu vào Monads. Tôi đã trở lại làm việc và tập trung vào các công nghệ mà dự án của tôi yêu cầu.
Cái này hay đấy. Đó là một chút trừu tượng mặc dù. Tôi có thể tưởng tượng những người không biết những gì các đơn vị đã bị nhầm lẫn do thiếu các ví dụ thực tế.
Vì vậy, hãy để tôi cố gắng tuân thủ và để thực sự rõ ràng tôi sẽ làm một ví dụ trong C #, mặc dù nó sẽ trông xấu xí. Tôi sẽ thêm Haskell tương đương vào cuối và cho bạn thấy đường cú pháp Haskell tuyệt vời, nơi IMO, các đơn vị thực sự bắt đầu trở nên hữu ích.
Được rồi, vì vậy một trong những Monads dễ nhất được gọi là "Có thể là monad" trong Haskell. Trong C #, loại Có thể được gọi Nullable<T>
. Về cơ bản, đây là một lớp nhỏ chỉ gói gọn khái niệm giá trị hợp lệ và có giá trị hoặc là "null" và không có giá trị.
Một điều hữu ích để gắn bó bên trong một đơn nguyên để kết hợp các giá trị của loại này là khái niệm về sự thất bại. Tức là chúng tôi muốn có thể xem xét nhiều giá trị nullable và trả vềnull
ngay khi bất kỳ một trong số chúng là null. Điều này có thể hữu ích nếu bạn, ví dụ, tìm kiếm nhiều khóa trong từ điển hoặc thứ gì đó, và cuối cùng bạn muốn xử lý tất cả các kết quả và kết hợp chúng bằng cách nào đó, nhưng nếu bất kỳ khóa nào không có trong từ điển, bạn muốn trở lại null
cho tất cả mọi thứ. Sẽ rất tẻ nhạt khi phải kiểm tra thủ công từng lần tra cứu
null
và trả lại, vì vậy chúng tôi có thể ẩn kiểm tra này bên trong toán tử liên kết (đó là điểm của các đơn vị, chúng tôi ẩn việc giữ sách trong toán tử liên kết giúp mã dễ dàng hơn sử dụng vì chúng ta có thể quên các chi tiết).
Đây là chương trình thúc đẩy toàn bộ sự việc (Tôi sẽ xác định
Bind
sau, đây chỉ là để cho bạn thấy lý do tại sao nó tốt đẹp).
class Program
{
static Nullable<int> f(){ return 4; }
static Nullable<int> g(){ return 7; }
static Nullable<int> h(){ return 9; }
static void Main(string[] args)
{
Nullable<int> z =
f().Bind( fval =>
g().Bind( gval =>
h().Bind( hval =>
new Nullable<int>( fval + gval + hval ))));
Console.WriteLine(
"z = {0}", z.HasValue ? z.Value.ToString() : "null" );
Console.WriteLine("Press any key to continue...");
Console.ReadKey();
}
}
Bây giờ, bỏ qua một lúc rằng đã có hỗ trợ để thực hiện việc này Nullable
trong C # (bạn có thể thêm ints nullable với nhau và bạn nhận được null nếu một trong hai null). Hãy giả vờ rằng không có tính năng này và đó chỉ là một lớp do người dùng định nghĩa không có phép thuật đặc biệt. Vấn đề là chúng ta có thể sử dụng Bind
hàm để liên kết một biến với nội dung của Nullable
giá trị của chúng ta và sau đó giả vờ rằng không có gì lạ xảy ra, và sử dụng chúng như ints bình thường và chỉ cần thêm chúng lại với nhau. Chúng tôi quấn kết quả trong một nullable ở cuối, và nullable mà một trong hai sẽ được null (nếu bất kỳ f
, g
hoặc h
lợi nhuận null) hoặc nó sẽ là kết quả của cách tổng hợp f
, g
vàh
cùng với nhau. (điều này tương tự với cách chúng ta có thể liên kết một hàng trong cơ sở dữ liệu với một biến trong LINQ và thực hiện công cụ với nó, an toàn với kiến thức rằngBind
toán tử sẽ đảm bảo rằng biến sẽ chỉ được truyền các giá trị hàng hợp lệ).
Bạn có thể chơi với cái này và thay đổi bất kỳ f
,g
và h
để trở về null và bạn sẽ thấy rằng toàn bộ điều sẽ trả về null.
Vì vậy, rõ ràng toán tử liên kết phải thực hiện việc kiểm tra này cho chúng tôi và bảo lãnh trả về null nếu nó gặp giá trị null và nếu không thì chuyển qua giá trị bên trong Nullable
cấu trúc vào lambda.
Đây là Bind
toán tử:
public static Nullable<B> Bind<A,B>( this Nullable<A> a, Func<A,Nullable<B>> f )
where B : struct
where A : struct
{
return a.HasValue ? f(a.Value) : null;
}
Các loại ở đây giống như trong video. Nó cần một M a
( Nullable<A>
cú pháp C # cho trường hợp này) và một hàm từ a
đến
M b
( Func<A, Nullable<B>>
trong cú pháp C #) và nó trả về mộtM b
( Nullable<B>
).
Mã chỉ đơn giản kiểm tra xem nullable có chứa một giá trị hay không và nếu nó trích xuất nó và chuyển nó vào hàm, thì nó chỉ trả về null. Điều này có nghĩa là Bind
toán tử sẽ xử lý tất cả logic kiểm tra null cho chúng tôi. Nếu và chỉ khi giá trị mà chúng ta gọi
Bind
là không có giá trị thì giá trị đó sẽ được "chuyển qua" cho hàm lambda, nếu không chúng ta sẽ bảo lãnh sớm và toàn bộ biểu thức là null. Điều này cho phép mã mà chúng ta viết bằng cách sử dụng đơn nguyên hoàn toàn không có hành vi kiểm tra null này, chúng ta chỉ cần sử dụng Bind
và nhận một biến bị ràng buộc với giá trị bên trong giá trị đơn nguyên ( fval
,
gval
và hval
trong các mã ví dụ) và chúng ta có thể sử dụng chúng an toàn trong kiến thức Bind
sẽ chăm sóc kiểm tra chúng vô giá trị trước khi đưa chúng đi cùng.
Có những ví dụ khác về những điều bạn có thể làm với một đơn nguyên. Ví dụ, bạn có thể làm cho Bind
toán tử chăm sóc một luồng ký tự đầu vào và sử dụng nó để viết các tổ hợp trình phân tích cú pháp. Mỗi trình kết hợp trình phân tích cú pháp sau đó có thể hoàn toàn không biết gì về những thứ như theo dõi ngược, lỗi trình phân tích cú pháp, v.v. và chỉ kết hợp các trình phân tích cú pháp nhỏ hơn với nhau như thể mọi thứ sẽ không bao giờ sai, an toàn khi biết rằng việc triển khai thông minh sẽ Bind
loại bỏ mọi logic đằng sau bit khó. Sau đó, có thể ai đó thêm đăng nhập vào đơn nguyên, nhưng mã sử dụng đơn vị không thay đổi, vì tất cả các phép thuật xảy ra trong định nghĩa củaBind
toán tử, phần còn lại của mã không thay đổi.
Cuối cùng, đây là việc thực hiện cùng một mã trong Haskell ( --
bắt đầu một dòng bình luận).
-- Here's the data type, it's either nothing, or "Just" a value
-- this is in the standard library
data Maybe a = Nothing | Just a
-- The bind operator for Nothing
Nothing >>= f = Nothing
-- The bind operator for Just x
Just x >>= f = f x
-- the "unit", called "return"
return = Just
-- The sample code using the lambda syntax
-- that Brian showed
z = f >>= ( \fval ->
g >>= ( \gval ->
h >>= ( \hval -> return (fval+gval+hval ) ) ) )
-- The following is exactly the same as the three lines above
z2 = do
fval <- f
gval <- g
hval <- h
return (fval+gval+hval)
Như bạn có thể thấy do
ký hiệu đẹp ở cuối làm cho nó trông giống như mã mệnh lệnh thẳng. Và thực sự đây là do thiết kế. Monads có thể được sử dụng để gói gọn tất cả những thứ hữu ích trong lập trình mệnh lệnh (trạng thái có thể thay đổi, IO, v.v.) và được sử dụng bằng cú pháp giống như mệnh lệnh tốt đẹp này, nhưng đằng sau màn cửa, tất cả chỉ là đơn nguyên và triển khai thông minh của toán tử liên kết! Điều thú vị là bạn có thể thực hiện các đơn nguyên của riêng mình bằng cách thực hiện >>=
và return
. Và nếu bạn làm như vậy, các đơn nguyên đó cũng sẽ có thể sử dụng do
ký hiệu, điều đó có nghĩa là về cơ bản bạn có thể viết các ngôn ngữ nhỏ của riêng mình chỉ bằng cách xác định hai chức năng!