“Ücretsiz Monad + Tercüman” kalıbı nedir?


95

İnsanları , özellikle veri erişimi bağlamında, Tercüman olan Ücretsiz Monad hakkında konuşurken gördüm . Bu örüntü nedir? Ne zaman kullanmak isteyebilirim? Nasıl çalışır ve nasıl uygularım?

Ben (örneğin mesaj anladığımız bu veri-erişimden modeli ayıran ilgili olduğunu). Tanınmış Havuz modelinden farkı nedir? Aynı motivasyona sahip görünüyorlar.

Yanıtlar:


138

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ı.


6
Neden 'özgür' bir monad deniyor?
Benjamin Hodgson,

14
"Özgür" adı kategori teorisinden gelir: ncatlab.org/nlab/show/free+object ancak bu, "minimum" monad olduğu anlamına gelir - yalnızca geçerli işlemlerin olduğu gibi monad işlemleri olduğu anlamına gelir " Unutulmuş "hepsi bu diğer yapı.
Boyd Stephen Smith Jr.

3
@ BenjaminHodgson: Boyd tamamen haklı. Merak etmediğin sürece fazla endişelenmem. Dan Piponi, BayHac'ta "özgür" ifadesinin ne anlama geldiğiyle ilgili harika bir konuşma yaptı . Slaytları ile birlikte takip etmeye çalışın çünkü videodaki görsel tamamen işe yaramaz.
Tikhon Jelvis

3
Nitepick: "Ücretsiz monad kısmı sadece [benim vurgum] Haskell'in standart monad olanaklarını (no-notation gibi) çok fazla özel kod yazmak zorunda kalmadan kullanarak yapabileceğiniz bir AST elde etmenin kullanışlı bir yoludur." "Sadece" dan daha fazlası (bildiğinizden emin olduğum gibi). Ücretsiz monadlar ayrıca, tercümanın do-notasyonu farklı olan ama aslında "aynı anlama gelen" programları ayırt etmesini imkansız kılan normalleştirilmiş bir program temsilidir .
sacundim

5
@sacundim: Yorumunuz hakkında ayrıntılı bilgi verir misiniz? Özellikle “Serbest monadlar” aynı zamanda, tercümanın yapması farklı fakat aslında "aynı" anlamına gelen programları birbirinden ayırt etmesini imkansız kılan normalleştirilmiş bir program temsilidir.
Giorgio

15

Serbest bir monad, temelde daha karmaşık bir şey yapmak yerine hesaplama ile aynı "şekilde" bir veri yapısı oluşturan bir monadtır. ( Orada bulunacak örnekleri. Online * Ben depo desen tamamen aşina değilim. Bu veri yapısı sonra tüketir ve faaliyetlerini yapan bir kod parçası geçirilir), ancak gelen ne okudum göründüğü Daha üst düzeyde bir mimari olması ve onu uygulamak için ücretsiz bir monad + tercüman kullanılabiliyordu. Öte yandan, ücretsiz monad + yorumlayıcısı aynı zamanda, ayrıştırıcılar gibi tamamen farklı şeyler uygulamak için de kullanılabilir.

* Bu modelin monalara özel olmadığını ve aslında ücretsiz uygulayıcılar veya ücretsiz oklarla daha verimli kodlar üretebileceğini belirtmekte fayda var . ( Parsers bunun başka bir örneğidir. )


Özür dilerim, Depo hakkında daha net davranmalıydım. (Herkesin bir iş sistemine / OO / DDD geçmişine sahip olmadığını unuttum!) Bir Havuz, temel olarak veri erişimini kapsıyor ve sizin için etki alanı nesnelerini yeniden sulandırıyor. Genellikle Bağımlılık İnversiyonu ile birlikte kullanılır - Repo'nun farklı uygulamalarını 'takabilirsiniz' (test için veya veritabanını veya ORM'yi değiştirmeniz gerekirse) yararlıdır. Alan kodu sadece çağrıları repository.Get()hiçbir bilgi ile nerede ondan alanı nesnesini oluyor.
Benjamin Hodgson
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.