osa1 github gitlab twitter cv rss

IO monadında hayat, monad transformerlar ve XMonad

April 22, 2012 - Tagged as: haskell, tr.

Bir süredir Haskell yazan biri olarak kafama takılan birkaç şeyden bahsedeceğim.

IO monadının içerisi yan etkilerle dolu, neredeyse imperative bir dünya. Her türlü mutable değişkenleri değiştirebilir(IORef, MVar, TMVar, MArray, ve benim aklıma gelmeyen/bilmediğim dahası), threadlar oluşturabilir, dosyalardan okuma/dosyalara yazma yapabilir, exception atıp yakalyabilir, ve bunlar gibi fonksiyonel programlarda kısıtlı ve fonksiyonel koddan ayrı tutulmaya çalışılan bir sürü şey yapabilirsiniz.

Haskell’de bir IO işleminin sonucunu, IO işleminden ayıramıyorsunuz. Tüm IO işlemini bir başlangıç durumu ve bunun üzerine yapılacak hareketlerin sıralı bir listesi gibi düşünebiliriz. Tüm Haskell programları IO () tipine sahip bir main fonksiyonu ile çalıştırılır ve tüm IO işlemleri bu main fonksiyonunun parametre olarak aldığı1bu IO monadı üzerinde çalışmak zorunda. Programın ömrü boyunca tek bir IO monadı olabiliyor ve bu da main fonnksiyonunda kullanılan IO monadı. Bu şu anlama geliyor, başka bir fonksiyondan IO işlemi yapacaksanız, bu mainden çağırılmak zorunda, veya mainden çağırılan başka bir IO fonksiyonundan.

Bu iki pratikte şu anlama geliyor: Hiçbir saf fonksiyondan IO yapamazsınız ve IO yapan fonksiyonları çağıramazsınız. Saf fonksiyonlarla yan etkilere sahip fonksiyonları birbirinden ayırmak zorundasınız yani2

Bu bir yandan kullanıcıyı kısıtlarken bir yandan daha fonksiyonel programlar yapmamızı sağlıyor. Yani zaten örneğin ML dillerinde veya Scheme gibi fonksiyonel bir yol izleyen dillerde fonksiyonel kısmı yan etkilerden ayırmak beklenen bir davranış. Haskell ile bir oyun yaptığınızda, çizim yapan tüm fonksiyonlarınız, diğer fonksiyonlardan ayrı olmak zorunda. Oyun durumuyla alakalı birşeyler hesapladığınız bir fonksiyonda ekrana birşeyler çizdiremezsiniz.

Aslında iş biraz daha abartı, monadları bir “içerik”(state, reader, writer monadlarındaki gibi) veya bir başlangıç durumu + “hareketler”(IO monadındaki gibi) tuttuğunu düşünün. Monadlar ile çalışan fonksiyonlar genelde monadın o anki içeriğini alıp, yeni bir içerik ile beraber bir değer dönen fonksiyonlar oluyor. Bu fonksiyonların birden fazla monad ile çalışmasını daha zor bir hale getiriyor ve farkında olmadan, örneğin oyun durumunu güncelleyen, state monadı ile çalışan fonksiyonlarınızla list monadı ile non-deterministic hesaplamalar yaptığınız fonksiyonları birbirinden ayırmış oluyorsunuz.

Bu ilk başta güzel gözükse de, birden fazla monad ile çalışmak isteyebileceğiniz çok fazla durum var, ve bu durumlar için çok güzel bir çözüm var aslında.

Monad transformerlar

Neden “transformer” dendiğini anlamadığım bu şeyler aslında3 yukarıda bahsettiğim monad tanımındaki “içerik” kısmında, içerik olarak bir de başka bir monadı daha tutan monadlar oluyor. Bu sayede bu içerikdeki monad üzerinde de işlemler yapabiliyorsunuz. Bu arada monad transformer aslında normal bir monad, hiçbir farkı yok. Yani 4-5 tane monadı bu şekilde iç içe koyabilir, bir fonksiyonda bu monadlar ile çalışabilirsiniz.

Bunun çok da mantıklı olmadığı bariz, bizim yapmaya çalıştığımız zaten fonksiyonları sorumluluklarına göre birbirinden ayırıp, programı daha modüler, anlaşılır, hatasız bir hale getirmek ve bu yaptığımız tam tersi olmuş oluyor. Reddit ortamında bu son zamanlarda şöyle bir mesajda tartışıldı.

Yine de bazı durumlarda gerekli ve alternatifiniz olmayabiliyor. Örneğin XMonad’daki gibi.

XMonad’da programın değişken durumu XState, çalışma süresi boyunca değişmeyecek bazı ayarları XConf veri yapısında tutuluyor. Bilmeyenler için, kendisi bir tiling window manager, ve bu da sürekli IO yapması(çizimler vs.) gerektiği anlamına geliyor. Reader monadı tam olarakXConfun tüm fonksyonlara açıkça parametre olarak göndermeden fonksiyonlar arasında paylaşılması için, State monadı da tam olarak XStatei paylaşma ve değiştirme işi için uygun. Çizim için de IO monadı gerekiyor. XMonad fonksiyonları çoğu zaman bu 3ünü birden kullandığından, şöyle bir transformer oluşturmuşlar:

newtype X a = X (ReaderT XConf (StateT XState IO) a)
    deriving (Functor, Monad, MonadIO, MonadState XState, MonadReader XConf, Typeable)

Üç monadı da içeren bir transformer4 Buna da X adını vermişler(süper isim değil mi :) . Bu sayede örneğin (WindowSet -> WindowSet) -> X () tipinde bir fonksiyondan, ask gibi bir reader monad fonksiyonu, modify gibi bir state monad fonksiyonu ve IO fonksiyonları çağırabiliyorlar.

Aslında bunun gibi içerisinde IO monadı bulunan transformerlarda IO yapabilmeyi kolaylaştırmak için bir typeclass bile var, MonadIO:

class Monad m => MonadIO m where
    liftIO :: IO a -> m a

Tam detaylarını bilmesem de, benim anladığım, GeneralizedNewtypeDeriving eklentisi ile GHC’de IO monadını sahip herhangi bir monad transformerı otomatik olarak MonadIO haline getirebiliyorsunuz. Detaylar şurda.

XMonad kodunu okumaya devam ediyorum, öğrenilecek çok şey var. Ben şunu söyleyecektim aslında, eğer kontrollü bir şekilde yapılmazsa, transformerlar ve özellikle IO çok tehlikeli olabiliyor.

Bir süredir geliştirdiğim bir server var, tüm socket işlemleri IO monadı ile çalıştğından çoğu fonksiyonum IO monadı ile çalışıyor. Sunucu multi-threaded, ve threadlar arasında senkronizasyonu sağlamak için tuttuğum MVarlar IO monadında değiştirilebiliyor. Bu durumda çoğu fonksiyonumda MVarları değiştirip, yeni threadlar oluşturabiliyor, file IO vb. her türlü yan etkili şeyler yapabiliyorum.

Kodun bu kısmını azaltmak için epey uğraştım, fonksiyonel bir çekirdek oluşturmaya çalıştım ama sonuç olarak sunucunun en temel işlemleri, threadlar arasında senkronize bir şekilde bazı durumları tutmak ve socket üzerinden okuyup yazmak. Her türlü IO monadı ile çalışmak zorundayım.

Programın kaynağını muhtemelen bir haftaya açmış olurum. Bu sırada Haskell ile yazılmış başka sunucuların kodlarını inceleyeceğim. GHC hackerlarından Simon P. Jonesun Tackling the awkward squad: monadic input/output, concurrency, exceptions, and foreign-language calls in Haskell makalesi tam olarak bu konulardan bahsediyor ve süper yazılmış. Haskell ile uğraşıyorsanız veya en azından yan etkilerin Haskell dünyasında nasıl karşılandığını merak ediyorsanız en azından ilk bölümü mutlaka okuyun.


Applicative functorlar için sorun yok, typeclass şu şekilde:

class (Functor f) => Applicative f where
    pure  :: a -> f a
    (<*>) :: f (a -> b) -> f a -> f b

Her applicative functor’un functor olması burda sağlanmış. Fakat monadlar için bu geçerli değil:

class Monad m where
    return :: a -> m a
    (>>=)  :: m a -> (a -> m b) -> m b

Bu tanım, Typeclassopediada belirtildiği gibi, şu şekilde olabilirdi:

class (Applicative m) => Monad m where
    (>>=) :: m a -> (a -> m b) -> m b

Bir kere her nedense bu böyle yapılmamış ve şimdi bu hale getirdiğimizde mevcut kodlardaki tüm returnleri pure ile, tüm liftMleri fmap ile, tüm apları <*> ile değiştirmek gerekiyor.

Bu bir de şu anlama geliyor, eğer bir monad yazmışsanız, birkaç satırda bunu applicative functor haline de getirebilirsiniz. Ve bunu yapmanız applicative tarzda programlamak isteyen birine kolaylık sağlamış olur, veya applicativelar üzerinde çalışan fonksiyonları kullanabilirsiniz. X monadında yaptıkları gibi:

instance Applicative X where
    pure = return
    (<*>) = ap

  1. Aslında parametre olarak mı alıyor, yoksa başka bir şekilde mi bilmiyorum. IO monad implementasyonu standartda belirtilmemiş, implementasyon detayı.↩︎

  2. unsafePerformIO ve benzerlerinin farkındayım.↩︎

  3. Monad stack falan daha uygun olurmuş sanki, ortada bir “dönüştürme” işlemi yok sonuçta.↩︎

  4. Bu arada hazır denk gelmişken ilginç birşeyden bahsedeceğim. Matematiksel olarak, tüm monadlar aslında applicative functor, ve tüm applicative functorlar aslında functor. Fakat Haskell’de, bazı geriye uyumluluk sorunları yüzünden bu tam olarak sağlanamıyor.↩︎