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
nextParametresi 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. nextKompozisyon için kullandığımızdan beri , p1türümüz programımızla aynı uzunluktadır (3 komut):
p1 :: DSL (DSL (DSL next))
Bu özel örnekte, nextböyle kullanmak biraz garip görünüyor, ancak eylemlerimizin farklı tür değişkenleri olmasını istiyorsak önemlidir. Biz Basılan isteyebilirsiniz getve setörneğin.
nextAlanın her işlem için nasıl farklı olduğuna dikkat edin. Bu, DSLbir 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ı derivingetkinleştirerek örneği otomatik olarak oluşturmak için kullanabiliriz DeriveFunctor.
Bir sonraki adım, Freetürün kendisidir. Yani bizim AST temsil etmek kullandığım şey yapısını , üst üste inşa DSLtip. 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 nextaynı 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: doDSL ifadelerimizi daha iyi hale getirmek için gösterimi kullanabiliriz . Tek soru ne koymak için next? Peki, fikir Freeyapıyı kompozisyon için kullanmaktır , bu yüzden Returnher 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 Freeve Returnbiryere. Biz "asansör" içine bir DSL eylem yol: Ne mutlu ki, biz yararlanmak bir desen var Freehep-Aynı biz sarın olduğunu Freeve uygulamak Returniç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, p4zorunlu 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, followbir 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 followprogramlarımızda olduğu gibi getveya 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 endgiriş 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 aiçin herhangi a (gibi Free DSL String, Free DSL Intvb), bu sürümü sadece bir kabul Free DSL aiçin çalıştığını her olası abiz 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ı runIObir wherebloğun içine taşıyabilir safeRunIOve aynı efekti elde edebiliriz.)
Kodumuzu çalıştırmak IOyapabileceğimiz tek şey değil. Test için, State Maponun 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ı.