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 getvà set, 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.