Mẫu Free Monad + Interpreter là gì?


95

Tôi đã thấy mọi người nói về Free Monad với Interpreter , đặc biệt là trong bối cảnh truy cập dữ liệu. Mẫu này là gì? Khi nào tôi có thể muốn sử dụng nó? Làm thế nào nó hoạt động, và làm thế nào tôi sẽ thực hiện nó?

Tôi hiểu (từ các bài đăng như thế này ) rằng đó là về việc tách mô hình khỏi truy cập dữ liệu. Nó khác với mẫu Kho lưu trữ nổi tiếng như thế nào? Họ dường như có cùng động lực.

Câu trả lời:


138

Các mẫu thực tế là thực sự tổng quát hơn đáng kể so với chỉ truy cập dữ liệu. Đó là một cách nhẹ nhàng để tạo một ngôn ngữ dành riêng cho tên miền cung cấp cho bạn AST và sau đó có một hoặc nhiều thông dịch viên để "thực thi" AST theo cách bạn muốn.

Phần đơn nguyên miễn phí chỉ là một cách thuận tiện để có được AST mà bạn có thể lắp ráp bằng các cơ sở đơn nguyên tiêu chuẩn của Haskell (như ký hiệu) mà không phải viết nhiều mã tùy chỉnh. Điều này cũng đảm bảo DSL của bạn có thể kết hợp được : bạn có thể định nghĩa nó theo từng phần và sau đó đặt các phần lại với nhau theo cách có cấu trúc, cho phép bạn tận dụng các khái niệm trừu tượng thông thường của Haskell như các hàm.

Sử dụng một đơn vị miễn phí cung cấp cho bạn cấu trúc của DSL có thể kết hợp; tất cả bạn phải làm là chỉ định các mảnh. Bạn chỉ cần viết một kiểu dữ liệu bao gồm tất cả các hành động trong DSL của bạn. Những hành động này có thể làm bất cứ điều gì, không chỉ truy cập dữ liệu. Tuy nhiên, nếu bạn đã chỉ định tất cả các truy cập dữ liệu của mình dưới dạng hành động, bạn sẽ nhận được AST chỉ định tất cả các truy vấn và lệnh cho kho lưu trữ dữ liệu. Sau đó, bạn có thể diễn giải điều này theo cách bạn muốn: chạy nó với cơ sở dữ liệu trực tiếp, chạy nó với một giả, chỉ cần đăng nhập các lệnh để gỡ lỗi hoặc thậm chí thử tối ưu hóa các truy vấn.

Hãy xem xét một ví dụ rất đơn giản, ví dụ, một cửa hàng giá trị chính. Hiện tại, chúng tôi sẽ chỉ coi cả khóa và giá trị là chuỗi, nhưng bạn có thể thêm các loại với một chút nỗ lực.

data DSL next = Get String (String -> next)
              | Set String String next
              | End

Các nexttham số cho phép chúng ta kết hợp hành động. Chúng ta có thể sử dụng điều này để viết một chương trình có "foo" và đặt "thanh" với giá trị đó:

p1 = Get "foo" $ \ foo -> Set "bar" foo End

Thật không may, điều này là không đủ cho một DSL có ý nghĩa. Vì chúng tôi đã sử dụng nextcho thành phần, nên loại p1có cùng độ dài với chương trình của chúng tôi (tức là 3 lệnh):

p1 :: DSL (DSL (DSL next))

Trong ví dụ cụ thể này, việc sử dụng nextnhư thế này có vẻ hơi kỳ lạ, nhưng điều quan trọng là chúng ta muốn các hành động của mình có các biến loại khác nhau. Chúng tôi có thể muốn đánh máy getset, ví dụ.

Lưu ý cách nexttrường khác nhau cho mỗi hành động. Gợi ý này mà chúng ta có thể sử dụng nó để tạo DSLfunctor:

instance Functor DSL where
  fmap f (Get name k)          = Get name (f . k)
  fmap f (Set name value next) = Set name value (f next)
  fmap f End                   = End

Trên thực tế, đây là cách hợp lệ duy nhất để biến nó thành Functor, vì vậy chúng ta có thể sử dụng derivingđể tạo cá thể tự động bằng cách bật DeriveFunctortiện ích mở rộng.

Bước tiếp theo là Freeloại chính nó. Đó là những gì chúng tôi sử dụng để thể hiện cấu trúc AST của chúng tôi , xây dựng trên đầu DSLloại. Bạn có thể nghĩ về nó giống như một danh sách ở cấp độ loại , trong đó "khuyết điểm" chỉ là lồng một hàm functor như DSL:

-- compare the two types:
data Free f a = Free (f (Free f a)) | Return a
data List a   = Cons a (List a)     | Nil

Vì vậy, chúng ta có thể sử dụng Free DSL nextđể cung cấp cho các chương trình có kích thước khác nhau cùng loại:

p2 = Free (Get "foo" $ \ foo -> Free (Set "bar" foo (Free End)))

Trong đó có loại đẹp hơn nhiều:

p2 :: Free DSL a

Tuy nhiên, biểu thức thực tế với tất cả các hàm tạo của nó vẫn rất khó sử dụng! Đây là nơi mà phần đơn nguyên xuất hiện. Như tên gọi "đơn vị tự do" ngụ ý, Freelà một đơn vị giáo dục miễn là f(trong trường hợp này DSL) là một functor:

instance Functor f => Monad (Free f) where
  return         = Return
  Free a >>= f   = Free (fmap (>>= f) a)
  Return a >>= f = f a

Bây giờ chúng tôi đang nhận được ở đâu đó: chúng tôi có thể sử dụng doký hiệu để làm cho biểu thức DSL của chúng tôi đẹp hơn. Câu hỏi duy nhất là đưa vào để làm nextgì? Chà, ý tưởng là sử dụng Freecấu trúc cho bố cục , vì vậy chúng tôi sẽ chỉ đặt Returncho từng trường tiếp theo và để ký hiệu làm tất cả các hệ thống ống nước:

p3 = do foo <- Free (Get "foo" Return)
        Free (Set "bar" foo (Return ()))
        Free End

Điều này tốt hơn, nhưng vẫn còn một chút khó xử. Chúng tôi có Freevà ở Returnkhắp mọi nơi. Hạnh phúc thay, có một mô hình mà chúng ta có thể khai thác: cách chúng ta "nâng" một hành động DSL vào Freeluôn luôn giống như chúng ta bọc nó Freevà áp dụng Returncho next:

liftFree :: Functor f => f a -> Free f a
liftFree action = Free (fmap Return action)

Bây giờ, bằng cách sử dụng này, chúng ta có thể viết các phiên bản đẹp của từng lệnh và có DSL đầy đủ:

get key       = liftFree (Get key id)
set key value = liftFree (Set key value ())
end           = liftFree End

Sử dụng cái này, đây là cách chúng ta có thể viết chương trình của mình:

p4 :: Free DSL a
p4 = do foo <- get "foo"
        set "bar" foo
        end

Thủ thuật gọn gàng là trong khi p4trông giống như một chương trình bắt buộc nhỏ, nó thực sự là một biểu thức có giá trị

Free (Get "foo" $ \ foo -> Free (Set "bar" foo (Free End)))

Vì vậy, phần đơn nguyên miễn phí của mẫu đã cho chúng ta một DSL tạo ra các cây cú pháp với cú pháp đẹp. Chúng ta cũng có thể viết các cây con có thể ghép lại bằng cách không sử dụng End; ví dụ: chúng ta có thể followlấy khóa, lấy giá trị của nó và sau đó sử dụng khóa đó làm khóa:

follow :: String -> Free DSL String
follow key = do key' <- get key
                get key'

Bây giờ followcó thể được sử dụng trong các chương trình của chúng tôi giống như gethoặc set:

p5 = do foo <- follow "foo"
        set "bar" foo
        end

Vì vậy, chúng tôi cũng nhận được một số thành phần tốt đẹp và trừu tượng cho DSL của chúng tôi.

Bây giờ chúng ta có một cây, chúng ta đến nửa sau của mẫu: trình thông dịch. Chúng ta có thể diễn giải cây theo cách chúng ta thích chỉ bằng cách khớp mẫu trên nó. Điều này sẽ cho phép chúng tôi viết mã chống lại một kho lưu trữ dữ liệu thực IO, cũng như những thứ khác. Đây là một ví dụ chống lại một kho dữ liệu giả định:

runIO :: Free DSL a -> IO ()
runIO (Free (Get key k)) =
  do res <- getKey key
     runIO $ k res
runIO (Free (Set key value next)) =
  do setKey key value
     runIO next
runIO (Free End) = close
runIO (Return _) = return ()

Điều này sẽ vui vẻ đánh giá bất kỳ DSLđoạn nào , ngay cả một đoạn chưa kết thúc end. Hạnh phúc, chúng ta có thể tạo một phiên bản "an toàn" của chức năng chỉ chấp nhận các chương trình đã đóng bằng endcách đặt chữ ký loại đầu vào thành (forall a. Free DSL a) -> IO (). Mặc dù chữ ký cũ chấp nhận một Free DSL acho bất kỳ a (như Free DSL String, Free DSL Intv.v.), phiên bản này chỉ chấp nhận một chữ ký Free DSL ahoạt động cho mọi khả năng amà chúng ta chỉ có thể tạo end. Điều này đảm bảo chúng tôi sẽ không quên đóng kết nối khi hoàn tất.

safeRunIO :: (forall a. Free DSL a) -> IO ()
safeRunIO = runIO

(Chúng tôi không thể chỉ bắt đầu bằng việc đưa ra runIOloại hình này bởi vì nó sẽ không hoạt động đúng cho cuộc gọi đệ quy của chúng tôi. Tuy nhiên, chúng ta có thể di chuyển các định nghĩa về runIOthành một wherekhối trong safeRunIOvà nhận được tác dụng tương tự mà không lộ cả hai phiên bản của hàm.)

Chạy mã của chúng tôi IOkhông phải là điều duy nhất chúng tôi có thể làm. Để thử nghiệm, chúng tôi có thể muốn chạy nó chống lại thuần túy State Map. Viết ra mã đó là một bài tập tốt.

Vì vậy, đây là mô hình phiên dịch đơn + miễn phí. Chúng tôi tạo ra một DSL, tận dụng cấu trúc đơn nguyên miễn phí để làm tất cả hệ thống ống nước. Chúng tôi có thể sử dụng ký hiệu và các chức năng đơn tiêu chuẩn với DSL của chúng tôi. Sau đó, để thực sự sử dụng nó, chúng ta phải giải thích nó bằng cách nào đó; vì cây cuối cùng chỉ là một cấu trúc dữ liệu, chúng ta có thể diễn giải nó theo cách chúng ta thích cho các mục đích khác nhau.

Khi chúng ta sử dụng điều này để quản lý truy cập vào một kho lưu trữ dữ liệu bên ngoài, nó thực sự giống với mẫu Kho lưu trữ. Nó trung gian giữa kho dữ liệu của chúng tôi và mã của chúng tôi, tách hai ra. Tuy nhiên, theo một số cách, nó cụ thể hơn: "kho lưu trữ" luôn là DSL có AST rõ ràng mà sau đó chúng ta có thể sử dụng theo cách chúng ta muốn.

Tuy nhiên, bản thân mẫu này còn chung chung hơn thế. Nó có thể được sử dụng cho nhiều thứ không nhất thiết phải liên quan đến cơ sở dữ liệu bên ngoài hoặc lưu trữ. Nó có ý nghĩa bất cứ nơi nào bạn muốn kiểm soát tốt các hiệu ứng hoặc nhiều mục tiêu cho DSL.


6
Tại sao nó được gọi là một đơn vị 'miễn phí'?
Benjamin Hodgson

14
Tên "miễn phí" xuất phát từ lý thuyết danh mục: ncatlab.org/nlab/show/free+object nhưng nó có nghĩa là nó là "đơn vị tối thiểu" - chỉ hoạt động hợp lệ trên đó là hoạt động đơn nguyên, vì nó có " quên "tất cả đó là cấu trúc khác.
Boyd Stephen Smith Jr.

3
@BenjaminHodgson: Boyd hoàn toàn đúng. Tôi sẽ không lo lắng về nó quá nhiều trừ khi bạn chỉ tò mò. Dan Piponi đã có một cuộc nói chuyện tuyệt vời về ý nghĩa của "miễn phí" tại BayHac, rất đáng để xem xét. Hãy thử làm theo cùng với các slide của anh ấy vì hình ảnh trong video hoàn toàn vô dụng.
Tikhon Jelvis

3
Một nitlog: "Phần đơn nguyên miễn phí chỉ là [sự nhấn mạnh của tôi] một cách tiện dụng để có được AST mà bạn có thể lắp ráp bằng các cơ sở đơn nguyên tiêu chuẩn của Haskell (như ký hiệu) mà không phải viết nhiều mã tùy chỉnh." Nó không chỉ là "chỉ" mà (như tôi chắc chắn bạn biết). Các đơn vị tự do cũng là một đại diện chương trình được chuẩn hóa khiến người phiên dịch không thể phân biệt giữa các chương trình có dochú thích khác nhau nhưng thực sự "có nghĩa giống nhau".
sacundim

5
@sacundim: Bạn có thể giải thích về nhận xét của bạn? Đặc biệt là câu 'Các đơn vị tự do cũng là một đại diện chương trình được chuẩn hóa khiến người phiên dịch không thể phân biệt giữa các chương trình có ký hiệu khác nhau nhưng thực ra "có nghĩa giống nhau."'.
Giorgio

15

Một đơn vị tự do về cơ bản là một đơn vị xây dựng cấu trúc dữ liệu theo "hình dạng" giống như tính toán thay vì làm bất cứ điều gì phức tạp hơn. ( Có các ví dụ được tìm thấy trực tuyến. ) Cấu trúc dữ liệu này sau đó được chuyển đến một đoạn mã tiêu thụ nó và thực hiện các hoạt động. * Tôi không hoàn toàn quen thuộc với mẫu kho lưu trữ, nhưng từ những gì tôi đã đọc nó xuất hiện để trở thành một kiến ​​trúc cấp cao hơn và một trình thông dịch monad + miễn phí có thể được sử dụng để thực hiện nó. Mặt khác, trình thông dịch monad + miễn phí cũng có thể được sử dụng để thực hiện những thứ hoàn toàn khác nhau, chẳng hạn như trình phân tích cú pháp.

* Điều đáng chú ý là mẫu này không dành riêng cho các đơn nguyên và trên thực tế có thể tạo ra mã hiệu quả hơn với các ứng dụng miễn phí hoặc mũi tên miễn phí . ( Trình phân tích cú pháp là một ví dụ khác về điều này. )


Xin lỗi, tôi nên nói rõ hơn về Kho lưu trữ. (Tôi quên rằng không phải ai cũng có hệ thống kinh doanh / nền OO / DDD!) Một kho lưu trữ về cơ bản đóng gói truy cập dữ liệu và bù nước cho các đối tượng miền cho bạn. Nó thường được sử dụng cùng với Nghịch đảo phụ thuộc - bạn có thể 'cắm' các triển khai khác nhau của Repo (hữu ích để thử nghiệm hoặc nếu bạn cần chuyển đổi cơ sở dữ liệu hoặc ORM). Đang miền chỉ gọi repository.Get()không có kiến thức về nơi nó nhận được đối tượng tên miền từ.
Benjamin Hodgson
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.