2012-05-28 35 views
33

Tôi dường như không tìm thấy bất kỳ lời giải thích nào về việc sử dụng ống kính nào trong các ví dụ thực tế. Đoạn ngắn này từ trang Hackage là gần nhất tôi đã tìm thấy:Ống kính được sử dụng/hữu ích cho là gì?

Mô-đun này cung cấp một cách thuận tiện để truy cập và cập nhật các phần tử của cấu trúc. Nó rất giống với Data.Accessors, nhưng hơi chung chung hơn một chút và có ít phụ thuộc hơn. Tôi đặc biệt thích cách nó xử lý gọn gàng các cấu trúc lồng nhau trong các trình đơn trạng thái.

Vì vậy, chúng được sử dụng để làm gì? Họ có những lợi ích và bất lợi nào so với các phương pháp khác? Tại sao họ cần?

+2

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. –

Trả lời

45

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ư vecXmatCol2 ở 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.

+2

Một điểm quan trọng mà câu trả lời của bạn bị thiếu là các ống kính là * lớp đầu tiên *, vì vậy bạn có thể xây dựng các abstractions khác từ chúng. Cú pháp ghi được xây dựng trong thất bại trong vấn đề đó. – jberryman

+2

Ngoài ra, tôi đã viết một bài đăng trên blog về các ống kính có thể hữu ích cho OP: http://www.haskellforall.com/2012/01/haskell-for-mainstream-programmers_28.html –

12

Ống kính cung cấp các cách thuận tiện để chỉnh sửa cấu trúc dữ liệu theo cách thống nhất, tổng hợp.

Nhiều chương trình được xây dựng xung quanh các hoạt động sau đây:

  • xem một thành phần của một (có thể lồng nhau) cấu trúc dữ liệu
  • lĩnh vực cập nhật (có thể lồng nhau) cấu trúc dữ liệu

Ống kính cung cấp hỗ trợ ngôn ngữ để xem và chỉnh sửa cấu trúc theo cách đảm bảo chỉnh sửa của bạn nhất quán; các chỉnh sửa đó có thể được soạn thảo dễ dàng; và rằng cùng một mã có thể được sử dụng để xem các phần của một cấu trúc, như để cập nhật các phần của cấu trúc.

Ống kính do đó làm cho nó dễ dàng để viết chương trình từ quan điểm vào cấu trúc; và từ các cấu trúc trở lại các khung nhìn (và các trình soạn thảo) cho các cấu trúc đó. Họ dọn sạch rất nhiều người truy cập hồ sơ và người định cư.

Pierce et al. ống kính phổ biến, ví dụ: trong số Quotient Lenses paper và triển khai cho Haskell hiện được sử dụng rộng rãi (ví dụ: fclabels và trình truy cập dữ liệu).

Đối với trường hợp sử dụng cụ thể, xem xét:

  • giao diện đồ họa người dùng, mà người dùng được chỉnh sửa thông tin một cách có cấu trúc
  • parsers và máy in khá
  • trình biên dịch
  • đồng bộ hóa cập nhật cấu trúc dữ liệu
  • cơ sở dữ liệu và lược đồ

và nhiều tình huống khác mà bạn có mô hình cấu trúc dữ liệu của thế giới và chế độ xem có thể chỉnh sửa trên dữ liệu đó.

6

Một lưu ý bổ sung thường bị bỏ qua là các ống kính thực hiện khái niệm "chung truy cập và cập nhật" chung. Ống kính có thể được viết cho tất cả mọi thứ, bao gồm các đối tượng giống như chức năng. Nó đòi hỏi một chút về tư duy trừu tượng để đánh giá cao này, vì vậy hãy để tôi chỉ cho bạn một ví dụ về sức mạnh của ống kính:

at :: (Eq a) => a -> Lens (a -> b) b 

Sử dụng at bạn thực sự có thể truy cập và thao tác các chức năng với nhiều đối số tùy thuộc vào đối số trước đó. Chỉ cần ghi nhớ rằng Lens là một danh mục. Đây là một thành ngữ rất hữu ích cho các chức năng điều chỉnh cục bộ hoặc những thứ khác.

Bạn cũng có thể truy cập dữ liệu bằng tài sản hoặc cơ quan đại diện thay thế:

polar :: (Floating a, RealFloat a) => Lens (Complex a) (a, a) 
mag :: (RealFloat a) => Lens (Complex a) a 

Bạn có thể đi xa hơn bằng văn bản ống kính để truy cập băng cá nhân của một tín hiệu Fourier-chuyển và nhiều hơn nữa.

Các vấn đề liên quan