Gerçek kalıp aslında sadece veri erişiminden çok daha genel. Size bir AST veren alana özgü bir dil yaratmanın ve ardından AST'yi istediğiniz gibi "yürütmesi" için bir veya daha fazla tercümana sahip olmanın hafif bir yoludur.
Ücretsiz monad kısmı, Haskell'in standart monad olanaklarını (no-notasyon gibi) kullanarak çok sayıda özel kod yazmanıza gerek kalmadan monte edebileceğiniz bir AST elde etmenin kullanışlı bir yoludur. Bu aynı zamanda DSL'nizin bir araya getirilebilmesini sağlar: parçaları parçalar halinde tanımlayabilir ve parçaları Haskell'in işlevler gibi normal soyutlamalarından yararlanmanıza izin verecek şekilde yapılandırılmış bir şekilde bir araya getirebilirsiniz.
Ücretsiz bir monad kullanmak, size bir beste edilebilir DSL'in yapısını verir ; Tek yapmanız gereken parçaları belirtmektir. DSL'nizdeki tüm eylemleri kapsayan bir veri türü yazmanız yeterlidir. Bu işlemler sadece veri erişimi değil, her şeyi yapıyor olabilir. Ancak, tüm verilerinizin erişimlerini eylemler olarak belirtirseniz, tüm sorguları ve komutları veri deposuna belirten bir AST alırsınız. Daha sonra istediğiniz gibi yorumlayabilirsiniz: Canlı bir veritabanına karşı çalıştırın, alaycısına karşı çalıştırın, sadece hata ayıklama için komutları kaydedin veya sorguları optimize etmeyi deneyin.
Diyelim ki anahtar değer deposu için çok basit bir örneğe bakalım. Şimdilik hem anahtarları hem de değerleri dizge olarak ele alacağız, ancak biraz çaba harcayan türler ekleyebilirsiniz.
data DSL next = Get String (String -> next)
| Set String String next
| End
next
Parametresi bize eylemleri araya getirme imkanı veriyor. Bunu "foo" alan ve bu değeri "bar" ayarlayan bir program yazmak için kullanabiliriz:
p1 = Get "foo" $ \ foo -> Set "bar" foo End
Ne yazık ki, anlamlı bir DSL için bu yeterli değil. next
Kompozisyon için kullandığımızdan beri , p1
türümüz programımızla aynı uzunluktadır (3 komut):
p1 :: DSL (DSL (DSL next))
Bu özel örnekte, next
böyle kullanmak biraz garip görünüyor, ancak eylemlerimizin farklı tür değişkenleri olmasını istiyorsak önemlidir. Biz Basılan isteyebilirsiniz get
ve set
örneğin.
next
Alanın her işlem için nasıl farklı olduğuna dikkat edin. Bu, DSL
bir functor yapmak için kullanabileceğimizi ima ediyor :
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
Aslında, bu bir Functor yapmak için tek geçerli yoldur, bu yüzden uzantıyı deriving
etkinleştirerek örneği otomatik olarak oluşturmak için kullanabiliriz DeriveFunctor
.
Bir sonraki adım, Free
türün kendisidir. Yani bizim AST temsil etmek kullandığım şey yapısını , üst üste inşa DSL
tip. Sen bir listesi gibi düşünebiliriz tipi "eksileri" gibi bir functor yuvalama düzeyi, DSL
:
-- compare the two types:
data Free f a = Free (f (Free f a)) | Return a
data List a = Cons a (List a) | Nil
Bu yüzden Free DSL next
aynı büyüklükteki programları aynı tipte vermek için kullanabiliriz :
p2 = Free (Get "foo" $ \ foo -> Free (Set "bar" foo (Free End)))
Hangisi daha güzel tipte:
p2 :: Free DSL a
Ancak, tüm kurucuları ile gerçek ifade kullanımı hala çok garip! Monad kısmının girdiği yer burasıdır. "Serbest monad" adının da belirttiği gibi Free
, bir monad f
(bu durumda DSL
) bir functor olduğu sürece :
instance Functor f => Monad (Free f) where
return = Return
Free a >>= f = Free (fmap (>>= f) a)
Return a >>= f = f a
Şimdi bir yerlere geliyoruz: do
DSL ifadelerimizi daha iyi hale getirmek için gösterimi kullanabiliriz . Tek soru ne koymak için next
? Peki, fikir Free
yapıyı kompozisyon için kullanmaktır , bu yüzden Return
her bir sonraki alana koyup , tüm sıhhi tesisatın sıhhi tesisat yapmasına izin verelim:
p3 = do foo <- Free (Get "foo" Return)
Free (Set "bar" foo (Return ()))
Free End
Bu daha iyi, ama yine de biraz garip. Biz Free
ve Return
biryere. Biz "asansör" içine bir DSL eylem yol: Ne mutlu ki, biz yararlanmak bir desen var Free
hep-Aynı biz sarın olduğunu Free
ve uygulamak Return
için next
:
liftFree :: Functor f => f a -> Free f a
liftFree action = Free (fmap Return action)
Şimdi, bunu kullanarak, komutlarımızın her birinin güzel versiyonlarını yazabilir ve tam bir DSL'e sahip olabiliriz:
get key = liftFree (Get key id)
set key value = liftFree (Set key value ())
end = liftFree End
Bunu kullanarak programımızı şöyle yazabiliriz:
p4 :: Free DSL a
p4 = do foo <- get "foo"
set "bar" foo
end
Düzgün bir numara, p4
zorunlu bir program gibi görünse de, aslında değeri olan bir ifadedir.
Free (Get "foo" $ \ foo -> Free (Set "bar" foo (Free End)))
Böylece, desenin serbest monad kısmı bize güzel sözdizimine sahip sözdizimi ağaçları üreten bir DSL'yi edindi. Kullanılamayan beste ağaçları da yazabiliriz End
; örneğin, follow
bir anahtar alan, değerini alan ve bunu anahtar olarak kullanan kişi olabilir:
follow :: String -> Free DSL String
follow key = do key' <- get key
get key'
Şimdi follow
programlarımızda olduğu gibi get
veya kullanılabilir set
:
p5 = do foo <- follow "foo"
set "bar" foo
end
Böylece DSL için de güzel bir kompozisyon ve soyutlama elde ediyoruz.
Şimdi bir ağacımız olduğuna göre, kalıbın ikinci yarısına geçiyoruz: tercüman. Ağacı yorumlayabiliriz ancak üzerinde desen eşleştirerek seviyoruz. Bu IO
, diğerlerinin yanı sıra gerçek bir veri deposuna karşı kod yazmamızı sağlar . İşte varsayımsal bir veri deposuna karşı bir örnek:
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 ()
Bu DSL
, sonu gelmeyen bir parçayı bile mutlu bir şekilde değerlendirir end
. Mutlu bir şekilde, yalnızca end
giriş türünü imza ayarlayarak kapalı olan programları kabul eden işlevin "güvenli" bir sürümünü yapabiliriz (forall a. Free DSL a) -> IO ()
. Eski imza bir kabul ederken Free DSL a
için herhangi a
(gibi Free DSL String
, Free DSL Int
vb), bu sürümü sadece bir kabul Free DSL a
için çalıştığını her olası a
biz sadece birlikte oluşturabilir -ki end
. Bu, işimiz bittiğinde bağlantıyı kapatmayı unutmayacağımızı garanti eder.
safeRunIO :: (forall a. Free DSL a) -> IO ()
safeRunIO = runIO
( runIO
Özyinelemeli çağrımız için düzgün bir şekilde çalışmadığı için bu türü vererek başlayamayız . Ancak, işlevin her iki sürümünü de göstermeden tanımını runIO
bir where
bloğun içine taşıyabilir safeRunIO
ve aynı efekti elde edebiliriz.)
Kodumuzu çalıştırmak IO
yapabileceğimiz tek şey değil. Test için, State Map
onun yerine saf bir ürün kullanmak isteyebiliriz . Bu kodu yazmak iyi bir alıştırma.
Yani bu ücretsiz monad + tercüman modeli. Tüm sıhhi tesisat yapmak için ücretsiz monad yapısından yararlanarak bir DSL yapıyoruz. DSL ile birlikte notasyonu ve standart monad işlevlerini kullanabiliriz. Sonra onu kullanmak için bir şekilde yorumlamamız gerekiyor; ağaç sonuçta sadece bir veri yapısı olduğundan, onu farklı amaçlarla istediğimiz gibi yorumlayabiliriz.
Bunu harici bir veri deposuna erişimleri yönetmek için kullandığımızda, gerçekten de Depo modeline benzer. Veri depomuz ve bizim kodumuz arasında arabuluculuk yapar, ikisini birbirinden ayırır. Yine de bazı yönlerden daha belirgindir: "depo" her zaman açık bir AST'ye sahip bir DSL'dir, sonra istediğimiz şekilde kullanabiliriz.
Bununla birlikte, modelin kendisi bundan daha geneldir. Dış veritabanlarını veya depolamayı gerektirmeyen birçok şey için kullanılabilir. Bir DSL için efektlerin kontrolünü veya çoklu hedefleri istediğiniz her yerde mantıklı.