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 next
tham 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 next
cho thành phần, nên loại p1
có 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 next
như 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 get
và set
, ví dụ.
Lưu ý cách next
trườ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 DSL
functor:
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 DeriveFunctor
tiện ích mở rộng.
Bước tiếp theo là Free
loạ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 DSL
loạ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ụ ý, Free
là 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 do
ký 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 next
gì? Chà, ý tưởng là sử dụng Free
cấu trúc cho bố cục , vì vậy chúng tôi sẽ chỉ đặt Return
cho 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ó Free
và ở Return
khắ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 Free
luôn luôn giống như chúng ta bọc nó Free
và áp dụng Return
cho 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 p4
trô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ể follow
lấ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ờ follow
có thể được sử dụng trong các chương trình của chúng tôi giống như get
hoặ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 end
cá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 a
cho bất kỳ a
(như Free DSL String
, Free DSL Int
v.v.), phiên bản này chỉ chấp nhận một chữ ký Free DSL a
hoạt động cho mọi khả năng a
mà 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 runIO
loạ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ề runIO
thành một where
khối trong safeRunIO
và 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 IO
khô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.