Yan etkiler neden Haskell'de monad olarak modellenmiştir?


172

Haskell'deki saf olmayan hesaplamaların neden monad olarak modellendiğine dair bazı işaretler verebilir mi?

Demek istediğim monad sadece 4 operasyonlu bir arayüz, peki yan etkileri modellemenin mantığı neydi?


15
Monadlar sadece iki işlemi tanımlar.
Dario

3
Peki dönüş ve başarısızlık ne olacak? ((>>) ve (>> =) yanı sıra)
bodacydo

55
İki işlem returnve (>>=). x >> yile aynıdır x >>= \\_ -> y(yani ilk argümanın sonucunu yoksayar). Hakkında konuşmuyoruz fail.
Porges

2
@Porges Neden başarısızlık hakkında konuşmuyorsunuz? Yani belki Yararlı, Ayrıştırıcı, vb biraz yararlı
alternatif

16
@monadic: failiçindedir Monadçünkü tarihsel bir kaza sınıfına; gerçekten ait MonadPlus. Varsayılan tanımının güvensiz olduğunu unutmayın.
JB.

Yanıtlar:


292

Bir işlevin yan etkileri olduğunu varsayalım. Ürettiği tüm efektleri giriş ve çıkış parametreleri olarak alırsak, işlev dış dünya için saftır.

Yani, saf olmayan bir işlev için

f' :: Int -> Int

RealWorld'ü değerlendirmeye ekliyoruz

f :: Int -> RealWorld -> (Int, RealWorld)
-- input some states of the whole world,
-- modify the whole world because of the side effects,
-- then return the new world.

sonra ftekrar saf. Parametreli bir veri türü tanımlıyoruz type IO a = RealWorld -> (a, RealWorld), bu yüzden RealWorld'ü birçok kez yazmamız gerekmiyor ve sadece yazabiliyoruz

f :: Int -> IO Int

Programcı için, bir RealWorld'ü doğrudan yönetmek çok tehlikelidir - özellikle, bir programcı ellerini RealWorld türünde bir değere alırsa, kopyalamayı deneyebilir , bu temel olarak imkansızdır. (Örneğin, tüm dosya sistemini kopyalamaya çalışın. Nereye koyarsınız?) Bu nedenle, IO tanımımız tüm dünyanın durumlarını da kapsar.

"Saf olmayan" fonksiyonların bileşimi

Eğer onları zincirleyemezsek, bu saf olmayan işlevler işe yaramaz. Düşünmek

getLine     :: IO String            ~            RealWorld -> (String, RealWorld)
getContents :: String -> IO String  ~  String -> RealWorld -> (String, RealWorld)
putStrLn    :: String -> IO ()      ~  String -> RealWorld -> ((),     RealWorld)

Biz istiyoruz

  • olsun konsolundan bir dosya adı,
  • o dosyayı oku ve
  • o dosyanın içeriğini konsola yazdırın .

Gerçek dünya devletlerine erişebilseydik nasıl yapardık?

printFile :: RealWorld -> ((), RealWorld)
printFile world0 = let (filename, world1) = getLine world0
                       (contents, world2) = (getContents filename) world1 
                   in  (putStrLn contents) world2 -- results in ((), world3)

Burada bir model görüyoruz. Fonksiyonlar şöyle adlandırılır:

...
(<result-of-f>, worldY) = f               worldX
(<result-of-g>, worldZ) = g <result-of-f> worldY
...

Böylece ~~~onları bağlamak için bir operatör tanımlayabiliriz :

(~~~) :: (IO b) -> (b -> IO c) -> IO c

(~~~) ::      (RealWorld -> (b,   RealWorld))
      ->                    (b -> RealWorld -> (c, RealWorld))
      ->      (RealWorld                    -> (c, RealWorld))
(f ~~~ g) worldX = let (resF, worldY) = f worldX
                   in g resF worldY

o zaman yazabiliriz

printFile = getLine ~~~ getContents ~~~ putStrLn

gerçek dünyaya dokunmadan.

"Impurification"

Şimdi dosya içeriğini de büyük harf yapmak istediğimizi varsayalım. Üst katmanlama saf bir işlevdir

upperCase :: String -> String

Ama bunu gerçek dünyaya dönüştürmek için geri dönmek zorunda IO String. Böyle bir işlevi kaldırmak kolaydır:

impureUpperCase :: String -> RealWorld -> (String, RealWorld)
impureUpperCase str world = (upperCase str, world)

Bu genelleştirilebilir:

impurify :: a -> IO a

impurify :: a -> RealWorld -> (a, RealWorld)
impurify a world = (a, world)

öyle ki impureUpperCase = impurify . upperCaseyazabiliriz

printUpperCaseFile = 
    getLine ~~~ getContents ~~~ (impurify . upperCase) ~~~ putStrLn

(Not: Normalde yazıyoruz getLine ~~~ getContents ~~~ (putStrLn . upperCase) )

Baştan beri monadlarla çalışıyorduk

Şimdi ne yaptığımızı görelim:

  1. (~~~) :: IO b -> (b -> IO c) -> IO cİki safsız fonksiyonu bir araya getiren bir operatör tanımladık
  2. impurify :: a -> IO aSaf bir değeri saf olmayan hale getiren bir fonksiyon tanımladık .

Şimdi kimlik yapmak (>>=) = (~~~)ve return = impurifyve görüyor musunuz? Bir monadımız var.


Teknik not

Gerçekten bir monad olduğundan emin olmak için hala kontrol edilmesi gereken birkaç aksiyom var:

  1. return a >>= f = f a

     impurify a                =  (\world -> (a, world))
    (impurify a ~~~ f) worldX  =  let (resF, worldY) = (\world -> (a, world )) worldX 
                                  in f resF worldY
                               =  let (resF, worldY) =            (a, worldX)       
                                  in f resF worldY
                               =  f a worldX
  2. f >>= return = f

    (f ~~~ impurify) worldX  =  let (resF, worldY) = f worldX 
                                in impurify resF worldY
                             =  let (resF, worldY) = f worldX      
                                in (resF, worldY)
                             =  f worldX
  3. f >>= (\x -> g x >>= h) = (f >>= g) >>= h

    Egzersiz olarak bırakıldı.


5
+1 ancak bunun özellikle G / Ç davasını kapsadığını belirtmek istiyorum. blog.sigfpe.com/2006/08/you-could-have-invented-monads-and.html oldukça benzer, ancak genelleme RealWorld... iyi, göreceksiniz.
ephemient

4
Bu açıklamanın Haskell için gerçekten geçerli olamayacağını unutmayın IO, çünkü ikincisi etkileşimi, eşzamanlılığı ve belirsizliği destekler. Daha fazla işaretçi için bu soruya verdiğim cevaba bakın.
Conal

2
@Conal GHC aslında IObu şekilde uygular , ancak RealWorldgerçek dünyayı temsil etmez, işlemleri düzenli tutmak için sadece bir jeton ("sihir" RealWorldGHC Haskell'in tek benzersiz tipidir)
Jeremy List

2
@JeremyList Anladığım kadarıyla GHC IO, bu temsilin ve standart olmayan derleyici sihrinin ( Ken Thompson'ın ünlü C derleyici virüsünü anımsatan) bir kombinasyonu ile uygular . Diğer türler için gerçek, her zamanki Haskell semantiği ile birlikte kaynak kodundadır.
Conal

1
@Klonal Yorumum GHC kaynak kodunun ilgili bölümlerini okumamdan kaynaklanıyordu.
Jeremy List

43

Haskell'deki saf olmayan hesaplamaların neden monad olarak modellendiğine dair bazı işaretler verebilir mi?

Bu soru yaygın bir yanlış anlama içeriyor. Safsızlık ve Monad bağımsız kavramlardır. Safsızlık edilir değil Monad ile modellenmiş. Aksine, IOzorunlu hesaplamayı temsil eden birkaç veri türü vardır . Ve bu türlerin bazıları için, arayüzlerinin küçük bir kısmı "Monad" olarak adlandırılan arayüz modeline karşılık gelir. Dahası, bilinen "saf / fonksiyonel / dentatif" bir açıklama yoktur IO(ve "günah kutusu" göz önüne alındığında bir tanesinin olması muhtemel değildir. amacıIO ), ancak World -> (a, World)anlamı olmakla ilgili yaygın olarak anlatılan bir hikaye vardır IO a. Bu hikaye doğru bir şekilde tarif edilemez IO, çünküIOeşzamanlılığı ve belirsizliği destekler. Hikaye, dünya ile orta hesaplamalı etkileşime izin veren deterministik hesaplamalar için bile işe yaramaz.

Daha fazla açıklama için bu cevaba bakınız .

Edit : Soruyu yeniden okurken, cevabım oldukça yolda olduğunu sanmıyorum. Zorunlu hesaplama modelleri, aynı soruda belirtildiği gibi, genellikle monad olur. Asker, monaditenin herhangi bir şekilde zorunlu hesaplamanın modellenmesini sağladığını varsaymayabilir.


1
@KennyTM: Ama RealWorldbelgelerin dediği gibi "derinden büyülü". Çalışma zamanı sisteminin ne yaptığını temsil eden bir jeton, aslında gerçek dünya hakkında hiçbir şey ifade etmiyor. Ekstra hile yapmadan bir "iplik" yapmak için yeni bir tane bile yaratamazsınız; saf yaklaşım, ne zaman çalışacağı konusunda çok fazla belirsizliğe sahip tek bir engelleme eylemi yaratacaktır.
CA McCann

4
Ayrıca, monads iddia ediyorum olan doğada aslında zorunluluk. Functor, değerleri katıştırılmış bazı yapıları temsil ediyorsa, monad örneği, bu değerlere dayalı olarak yeni katmanlar oluşturabileceğiniz ve düzleştirebileceğiniz anlamına gelir. Yani, fonksiyonun tek bir katmanına atadığınız anlam ne olursa olsun, bir monad, bir nedenden diğerine katı bir nedensellik kavramıyla sınırsız sayıda katman oluşturabileceğiniz anlamına gelir. Belirli durumlar kendiliğinden zorunlu bir yapıya sahip olmayabilir, ancak Monadgenel olarak gerçekten de vardır.
CA McCann

3
" MonadGenel" ile kastediyorum forall m. Monad m => ..., yani keyfi bir örnek üzerinde çalışmak. Rasgele bir monad ile yapabileceğiniz şeyler, neredeyse tamamen aynı şeylerdir IO: opak ilkelleri almak (sırasıyla işlev argümanları olarak veya kütüphanelerden), op-no'ları inşa edin returnveya bir değeri geri döndürülemez bir şekilde dönüştürerek (>>=). Keyfi bir monadda programlamanın özü, geri alınamaz eylemlerin bir listesini oluşturur: "X yapın, sonra Y yapın, sonra ...". Bana oldukça zor geliyor!
CA McCann

2
Hayır, burada hala benim fikrimi kaçırıyorsun. Elbette bu zihniyeti bu türlerin hiçbiri için kullanmazsınız, çünkü açık ve anlamlı bir yapıya sahiptirler. "Keyfi monadlar" dediğimde "hangisini seçemezsiniz"; buradaki perspektif nicelleştiricinin içindedir, bu yüzden mvaroluşçu düşünmek daha yararlı olabilir. Dahası, benim “ yorumum ” yasaların yeniden ifade edilmesidir; "do X" ifadelerinin listesi, üzerinden oluşturulan bilinmeyen yapıda tam anlamıyla serbest monoiddir (>>=); ve monad yasaları sadece endofunctor kompozisyonu ile ilgili monoid yasalarıdır.
CA McCann

3
Kısacası, tüm monadların birlikte tarif ettikleri en büyük alt sınır, geleceğe kör, anlamsız bir yürüyüştür. IOtam olarak patolojik bir durumdur, çünkü bu minimumdan neredeyse hiçbir şey sunmaz. Belirli durumlarda, türler daha fazla yapı ortaya çıkarabilir ve bu nedenle gerçek anlamı olabilir; ancak aksi halde bir monadın - yasalara dayalı - temel özellikleri, açık bir ifadeye olduğu kadar antitetiktir IO. İhracatçıları ihraç etmeden, ilkel eylemleri kapsamlı bir şekilde numaralandırmadan veya benzer bir şey yapmadan durum umutsuzdur.
CA McCann

13

Anladığım kadarıyla, Eugenio Moggi denilen biri , daha önce "monad" adı verilen daha önce belirsiz bir matematiksel yapının bilgisayar dillerindeki yan etkileri modellemek için kullanılabileceğini ve böylece anlamlarını Lambda hesabı kullanarak belirleyebileceğini fark etti. Haskell geliştirilirken saf olmayan hesaplamaların modellenmesinin çeşitli yolları vardı ( daha fazla ayrıntı için Simon Peyton Jones'un "saç gömleği" kağıdına bakın), ancak Phil Wadler monad'ları tanıttığında bunun Yanıt olduğunu açıkça ortaya koydu. Ve gerisi tarih.


3
Pek değil. Bir monadın yorumlamayı çok uzun bir süre için modelleyebileceği bilinmektedir (en azından "Topoi: Mantıksal Kategorik Bir Analiz") Öte yandan, güçlü yazılan işlevselliğe kadar monad tiplerini açıkça ifade etmek mümkün olmamıştır. diller geldi ve sonra Moggi iki ve ikisini bir araya getirdi
nomen

1
Belki de monad'ların harita sargısı olarak tanımlanıp tanımlanmadıklarını anlamak daha kolay olabilirdi.
aoeu256

9

Haskell'deki saf olmayan hesaplamaların neden monad olarak modellendiğine dair bazı işaretler verebilir mi?

Çünkü Haskell saf . Tip düzeyinde saf olmayan ve saf olanları ayırt etmek ve program akışlarını modellemek için matematiksel bir konsepte ihtiyacınız vardır . sırasıyla .

Bu, IO asaf olmayan bir hesaplamayı modelleyen bir türle sonuçlamanız gerektiği anlamına gelir . O zaman bu hesaplamaları bir araya getirmenin ( ) sırayla ( >>=) uygulandığı ve bir değeri (return ) en açık ve temel olanlarını .

Bu ikisiyle zaten bir monad tanımladınız (düşünmeden bile);)

Ek olarak, monadlar çok genel ve güçlü soyutlamalar sağlar , bu nedenle birçok tür kontrol akışı sequence, liftMveya özel sözdizimi gibi monadik işlevlerde uygun şekilde genelleştirilebilir , bu da saflığı böyle özel bir durum haline getirmez.

Daha fazla bilgi için işlevsel programlama ve benzersiz yazımdaki (bildiğim tek alternatif) monad'lara bakın .


6

Dediğiniz gibi Monad, çok basit bir yapı. Cevabın yarısı: Monadyan etki yapan fonksiyonlara verebileceğimiz ve bunları kullanabileceğimiz en basit yapı. İle Monadiki şey yapabiliriz: saf bir değeri yan etki değeri ( return) olarak ele alabiliriz ve yeni bir yan etki değeri ( >>=) elde etmek için yan etki değerine yan etki fonksiyonu uygulayabiliriz . Bunlardan herhangi birini yapma yeteneğini kaybetmek sakatlayıcı olacaktır, bu nedenle yan etki eden tipimizin "en azından" olması gerekir Monadve ortaya çıkıyorMonad gerekir ve şimdiye kadar ihtiyacımız olan her şeyi uygulamak yeterlidir.

Diğer yarısı: "olası yan etkilere" verebileceğimiz en ayrıntılı yapı nedir? Kesinlikle tüm olası yan etkilerin alanını bir set olarak düşünebiliriz (gerektiren tek işlem üyeliktir). İki yan etkiyi birbiri ardına yaparak birleştirebiliriz ve bu farklı bir yan etkiye yol açabilir (veya muhtemelen aynı - birincisi "kapatma bilgisayarı" ve ikincisi "dosya yazma" ise sonuç Bunları oluşturmak sadece "kapatma bilgisayarı" dır).

Peki, bu operasyon hakkında ne söyleyebiliriz? İlişkiseldir; yani, üç yan etkiyi birleştirirsek, birleştirmeyi hangi sırayla yaptığımız önemli değildir. Eğer yaparsak (dosya yaz sonra soketi okur) sonra bilgisayarı kapatırsak, yazma dosyası yapmakla aynıdır (soketi oku ve kapanır bilgisayar). Ancak değişmeli değildir: ("dosya yaz" sonra "dosya sil") ("dosyayı sil" sonra "dosya yaz") 'dan farklı bir yan etkidir. Ve bir kimliğimiz var: özel yan etki "yan etki yok" çalışıyor ("yan etki yok" sonra "dosya sil" sadece "dosya sil" ile aynı yan etkidir) Bu noktada herhangi bir matematikçi "Grup!" Ancak grupların tersleri vardır ve genel olarak bir yan etkiyi tersine çevirmenin bir yolu yoktur; "dosyayı sil" geri döndürülemez. Bu yüzden bıraktığımız yapı bir monoiddir, bu da yan etki eden fonksiyonlarımızın monad olması gerektiği anlamına gelir.

Daha karmaşık bir yapı var mı? Elbette! Olası yan etkileri dosya sistemi tabanlı efektlere, ağ tabanlı efektlere ve daha fazlasına bölebiliriz ve bu detayları koruyan daha ayrıntılı kompozisyon kuralları ortaya koyabiliriz. Ama yine de aşağıya iniyor: Monadçok basit ve önem verdiğimiz özelliklerin çoğunu ifade edecek kadar güçlü. (Özellikle, birliktelik ve diğer aksiyomlar, uygulamamızı, kombine uygulamanın yan etkilerinin, parçaların yan etkilerinin kombinasyonu ile aynı olacağından emin olarak, küçük parçalar halinde test etmemizi sağlar).


4

Aslında, G / Ç'yi işlevsel bir şekilde düşünmek için oldukça temiz bir yol.

Çoğu programlama dilinde giriş / çıkış işlemleri yaparsınız. Haskell'de değil yazma kodu hayal yapmak işlemleri için istediğiniz işlemlerin bir listesini oluşturmak için .

Monadlar tam olarak bunun için oldukça iyi bir sözdizimidir.

Monad'ların neden başka bir şeye karşı olduğunu bilmek istiyorsanız, cevap, insanların Haskell'i yaparken düşünebilecekleri G / Ç'yi temsil etmenin en iyi fonksiyonel yolu olduklarıdır.


3

AFAIK, bunun nedeni yazı sistemine yan etki kontrollerini dahil edebilmektir. Daha fazla bilgi edinmek istiyorsanız, SE-Radyo bölümlerini dinleyin : Bölüm 108: Fonksiyonel Programlama'da Simon Peyton Jones ve Haskell Bölüm 72: LINQ'da Erik Meijer


2

Yukarıda teorik arka plana sahip çok iyi ayrıntılı cevaplar var. Ama IO monad'ına ilişkin görüşümü vermek istiyorum Haskell programcısı değilim, bu yüzden oldukça naif veya hatta yanlış olabilir. Ama IO monad ile bir dereceye kadar uğraşmama yardım ettim (not, diğer monadlarla ilgili olmadığını unutmayın).

Öncelikle şunu söylemek istiyorum, "gerçek dünya" ile olan bu örnek benim için çok açık değil, çünkü önceki (gerçek dünya) devletlerine erişemiyoruz. Monad hesaplamaları ile ilgili olmayabilir, ancak genellikle haskell kodunda bulunan referans şeffaflığı anlamında arzu edilir.

Bu yüzden dilimizin (haskell) saf olmasını istiyoruz. Ancak, onlarsız bizim programımız yararlı olamayacağı için giriş / çıkış işlemlerine ihtiyacımız var. Ve bu operasyonlar doğası gereği saf olamaz. Dolayısıyla bununla başa çıkmanın tek yolu saf olmayan işlemleri kodun geri kalanından ayırmak zorundayız.

İşte monad geliyor. Aslında, benzer gerekli özelliklere sahip başka bir yapı bulunamayacağından emin değilim, ama mesele monad'ın bu özelliklere sahip olmasıdır, bu yüzden kullanılabilir (ve başarıyla kullanılır). Ana özellik ondan kaçamamamızdır. Monad arabiriminin, değerimiz etrafında monad'dan kurtulmak için işlemleri yoktur. Diğer (IO değil) monad'lar bu tür işlemleri sağlar ve kalıp eşleşmesine izin verir (örneğin Belki), ancak bu işlemler monad arayüzünde değildir. Diğer bir gerekli özellik, zincirleme işlemleri yeteneğidir.

Tip sistemi açısından neye ihtiyacımız olduğunu düşünürsek, herhangi bir vale etrafına sarılabilen yapıcı ile tipe ihtiyacımız olduğu gerçeğine geliriz. Oluşturucu özel olmalıdır, çünkü ondan kaçmayı yasaklıyoruz (yani desen eşleştirme). Ancak bu kurucuya değer katmak için işleve ihtiyacımız var (burada geri dönüş akla geliyor). Ve operasyonları zincirleme yoluna ihtiyacımız var. Bir süre düşünürsek, zincirleme işleminin >> = sahip olduğu tipine gelmesi gerçeğine geleceğiz. Böylece, monad'a çok benzeyen bir şeye geliyoruz. Sanırım şimdi bu yapı ile olası çelişkili durumları analiz edersek, monad aksiyomlarına geleceğiz.

Geliştirilen yapının safsızlıkla ortak bir yanı olmadığını unutmayın. Sadece saf olmayan işlemlerle uğraşmak istediğimiz özelliklere sahiptir, yani kaçış, zincirleme ve içeri girmenin bir yolu.

Şimdi bazı safsızlık işlemleri kümesi, bu seçilen monad IO içindeki dil tarafından önceden tanımlanmıştır. Bu işlemleri yeni saf olmayan operasyonlar oluşturmak için birleştirebiliriz. Ve tüm bu işlemlerin kendi tiplerinde IO olması gerekir. Bununla birlikte, bazı işlev türlerinde G / Ç varlığının bu işlevi saflaştırmadığını unutmayın. Ama anladığım kadarıyla, başlangıçta saf ve saf olmayan fonksiyonları ayırmak bizim fikrimiz olduğu için, saf fonksiyonları IO ile türlerine yazmak kötü bir fikirdir.

Son olarak, monad'ın saf olmayan operasyonları saf olanlara dönüştürmediğini söylemek istiyorum. Onları sadece etkili bir şekilde ayırmaya izin verir. (Tekrar ediyorum, bu sadece benim anlayışım)


1
Kontrol efektleri yazmanıza izin vererek programınızı kontrol etmenize yardımcı olurlar ve derleyicilerin sıralama hatalarınızı kontrol edebilmesi için işlevlerinizin yapabileceği efektleri kısıtlamak için monad'lar oluşturarak kendi DSL'lerinizi tanımlayabilirsiniz.
aoeu256

Aoeu256'dan gelen bu yorum, şu ana kadar yapılan tüm açıklamalarda eksik olan "neden" tir. (örneğin: Monad'lar insanlar için değil, derleyiciler içindir)
João Otero
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.