Chúng cung cấp bản tóm tắt rõ ràng về cập nhật dữ liệu và không bao giờ thực sự "cần thiết". Chúng chỉ cho phép bạn giải thích vấn đề theo một cách khác. Trong một số ngôn ngữ lập trình bắt buộc/"hướng đối tượng" như C, bạn có khái niệm quen thuộc về một số tập hợp giá trị (gọi chúng là "cấu trúc") và cách gắn nhãn mỗi giá trị trong bộ sưu tập (nhãn thường là được gọi là "trường").Điều này dẫn đến một định nghĩa như thế này:
typedef struct { /* defining a new struct type */
float x; /* field */
float y; /* field */
} Vec2;
typedef struct {
Vec2 col1; /* nested structs */
Vec2 col2;
} Mat2;
Sau đó bạn có thể tạo ra giá trị của loại mới định nghĩa này như sau:
Vec2 vec = { 2.0f, 3.0f };
/* Reading the components of vec */
float foo = vec.x;
/* Writing to the components of vec */
vec.y = foo;
Mat2 mat = { vec, vec };
/* Changing a nested field in the matrix */
mat.col2.x = 4.0f;
Tương tự trong Haskell, chúng tôi có các kiểu dữ liệu:
data Vec2 =
Vec2
{ vecX :: Float
, vecY :: Float
}
data Mat2 =
Mat2
{ matCol1 :: Vec2
, matCol2 :: Vec2
}
Loại dữ liệu này sau đó được sử dụng như sau:
let vec = Vec2 2 3
-- Reading the components of vec
foo = vecX vec
-- Creating a new vector with some component changed.
vec2 = vec { vecY = foo }
mat = Mat2 vec2 vec2
Tuy nhiên, trong Haskell, không có cách nào dễ dàng để thay đổi các trường lồng nhau trong cấu trúc dữ liệu. Điều này là do bạn cần phải tạo lại tất cả các đối tượng bao quanh giá trị mà bạn đang thay đổi, vì các giá trị Haskell là không thay đổi. Nếu bạn có một ma trận như trên trong Haskell, và muốn thay đổi ô trên bên phải trong ma trận, bạn phải viết điều này:
mat2 = mat { matCol2 = (matCol2 mat) { vecX = 4 } }
Nó hoạt động, nhưng có vẻ vụng về. Vì vậy, những gì một người nào đó đã đưa ra, về cơ bản là: Nếu bạn nhóm hai thứ lại với nhau: "getter" của một giá trị (như vecX
và matCol2
ở trên) với một hàm tương ứng, cho cấu trúc dữ liệu mà getter thuộc về, có thể tạo một cấu trúc dữ liệu mới với giá trị đó thay đổi, bạn có thể làm được nhiều thứ gọn gàng. Ví dụ:
data Data = Data { member :: Int }
-- The "getter" of the member variable
getMember :: Data -> Int
getMember d = member d
-- The "setter" or more accurately "updater" of the member variable
setMember :: Data -> Int -> Data
setMember d m = d { member = m }
memberLens :: (Data -> Int, Data -> Int -> Data)
memberLens = (getMember, setMember)
Có nhiều cách triển khai thấu kính; đối với văn bản này, giả sử một ống kính giống như trên:
type Lens a b = (a -> b, a -> b -> a)
I.e. nó là sự kết hợp của bộ thu thập và bộ đặt cho một số loại a
có trường loại b
, vì vậy memberLens
ở trên sẽ là Lens Data Int
. Điều này cho phép chúng ta làm gì?
Vâng, trước tiên hãy làm cho hai chức năng đơn giản mà trích xuất các getter và setter từ một ống kính:
getL :: Lens a b -> a -> b
getL (getter, setter) = getter
setL :: Lens a b -> a -> b -> a
setL (getter, setter) = setter
Bây giờ, chúng ta có thể bắt đầu trừu tượng hóa qua các công cụ. Hãy lấy tình huống trên một lần nữa, rằng chúng ta muốn sửa đổi một giá trị "hai tầng sâu". Chúng tôi thêm một cấu trúc dữ liệu với ống kính khác:
data Foo = Foo { subData :: Data }
subDataLens :: Lens Foo Data
subDataLens = (subData, \ f s -> f { subData = s }) -- short lens definition
Bây giờ, chúng ta hãy thêm một chức năng mà soạn hai ống kính:
(#) :: Lens a b -> Lens b c -> Lens a c
(#) (getter1, setter1) (getter2, setter2) =
(getter2 . getter1, combinedSetter)
where
combinedSetter a x =
let oldInner = getter1 a
newInner = setter2 oldInner x
in setter1 a newInner
Mã này được loại một cách nhanh chóng bằng văn bản, nhưng tôi nghĩ rằng đó là rõ ràng những gì nó làm : các getters chỉ đơn giản là sáng tác; bạn nhận được giá trị dữ liệu bên trong, và sau đó bạn đọc trường của nó. Bộ biến đổi, khi nó được cho là thay đổi một số giá trị a
với giá trị trường bên trong mới là x
, trước tiên lấy cấu trúc dữ liệu bên trong cũ, thiết lập trường bên trong của nó, và sau đó cập nhật cấu trúc dữ liệu ngoài với cấu trúc dữ liệu bên trong mới.
Bây giờ, chúng ta hãy làm một chức năng mà chỉ đơn giản tăng giá trị của một ống kính:
increment :: Lens a Int -> a -> a
increment l a = setL l a (getL l a + 1)
Nếu chúng ta có mã này, nó trở nên rõ ràng những gì nó làm:
d = Data 3
print $ increment memberLens d -- Prints "Data 4", the inner field is updated.
Bây giờ, bởi vì chúng tôi có thể sáng tác các ống kính, chúng tôi cũng có thể thực hiện việc này:
f = Foo (Data 5)
print $ increment (subDataLens#memberLens) f
-- Prints "Foo (Data 6)", the innermost field is updated.
Tất cả các gói thấu kính thực chất là gì? rap khái niệm về thấu kính này - nhóm một "setter" và "getter" thành một gói gọn gàng giúp họ dễ sử dụng. Trong một triển khai ống kính cụ thể, người ta có thể viết:
with (Foo (Data 5)) $ do
subDataLens . memberLens $= 7
Vì vậy, bạn nhận được rất gần với phiên bản C của mã; nó trở nên rất dễ dàng để sửa đổi các giá trị lồng nhau trong một cây cấu trúc dữ liệu.
Ống kính không có gì hơn thế này: một cách dễ dàng để sửa đổi các phần của một số dữ liệu. Bởi vì nó trở nên dễ dàng hơn nhiều lý do về các khái niệm nhất định vì chúng, chúng nhìn thấy sử dụng rộng rãi trong các tình huống mà bạn có bộ cấu trúc dữ liệu khổng lồ phải tương tác với nhau theo nhiều cách khác nhau.
Để biết ưu và khuyết điểm của ống kính, hãy xem a recent question here on SO.
Bạn có thể thích xem chương trình [Ống kính: A Functional Imperative] của Edward Kmett (http://www.youtube.com/watch?v=efv0SQNde5Q). Nó được trình bày trong Scala, nhưng việc dịch sang tính hữu dụng của ống kính trong Haskell phải rõ ràng. –