Interaction Nets, son zamanlar pek de bir şey anlamasam da ilgimi epey cezbediyor. Yani özellikleri benim açımdan epey ilginç duruyor ve Tarlox'ta neyi yanlış yaptığımı anlıyorum.
Tarlox neydi? Emekti. Benim iki üç ay boyunca odama kapanıp geliştirdiğim bir programlama dili. Doğrusunu söylemek istersem, yüzde yüz bana ait bir programlama dili diyemem. Bjarne Stroupstrap'ın (böyle mi yazılıyordu bu adamın ismi) "C with Classes"ı ilk çıkarttığı zamanı düşünün. Ortada biri dilden çok C'ye birkaç ekleme yapılmış bir geliştirme var. Bu da onun gibi, "Crafting Interpreters" isimli kitabın ilk bölümünde oluşturulan Lox programlama diline yapılan birkaç eklemeyi içeriyor.
Peki Tarlox'un eklemeleri neler? Github sayfasını açıp bakın. Ama en çok hoşuma giden şey şu: Paralel değişken hesaplamaları
Bir değişken atandığı zaman onun ataması ile kodun akışının kesilmemesi gerektiğine inanırım. Mesela şuna bakın:
var değişken1 = en_aşağı_bir_dakikalık();
var değişken2 = ölme_eşşeğim_ölme_fonksiyonu();
değişken1 ve değişken2'nin teker teker hesaplanmasının iki dakika sürdüğünü düşünelim. Eğer JavaScript'te bu kodu çalıştırıyorsak değişken1 ve değişken2 sırayla hesaplanacağı için toplam 4 dakika süreceğini düşünelim. Tabii hiçbir aklı başında yazılımcı buna razı kalmaz ve muhtemelen bu iki sorunun hesaplanması için iki adet thread çalıştıracaktır. Fakat bu threadlerin ikisinin de ayrı ayrı işlemesi demek bir kamyon senkronizasyon araçlarının da kodun içerisine dahil edilmesi demek. Tarlox'ta iki değişken aynı anda hesaplanır ve erişilmesi gereken ana kadar ikisi de programın ana akışını engellemez. Engellemeden kaçınmak istiyorsanız "is_ready" operatörü ile değişkenlerin hazır olup olmadığını sorgulayabiliriz.
Aslında bunun paralel hesaplamadan çok ağ işlemleri için de oldukça kullanışlı bir araç olduğunu düşünüyorum. Neticede ek bir thread illa ki "paralelizm" anlamına gelmez. Bir değişkene atanan fonksiyon pekala ağdan gelecek bir cevap için de bekliyor olabilir.
Bu tarz durumlarda tercih genelde "async/await" oluyor. Fena fikir değil, elbette. Herhalde o kadar Async/Await'i tasarlayan ve beğenerek kullanan kişilerin bir bildiği vardır. Yine de Async/Await'i beğenemiyorum doğrusu. Async Rust denen garabetin kendisi bile Async/Await'ten hoşlanmamaya sebep olabilir ancak bu kadar kolaya kaçmayacağım. Async/Await benim için tıpkı "exception"lar gibi kodun akışını karmaşıklaştıran, anlaşılmaz hale getiren ve deterministik anlamda "Şu zaman şu yapılmalı, bu zaman bu olmalı" fikrini takip etmeyi zorlaştıran bir şey. Kodun akışı öngörülebilir olmalı.
Erlang'i takdir etmeden geçemeyeceğim bu konuda. Kendi "preemption" mekanizması ile, "yani hangi threadin ne zaman işlemesi gerektiğine karar veren mekanizma" diyerek tasvir etmiş olalım, bekleyen threadleri ya da paralelde çalışmaya devam eden threadlerin ana akışı baltalamadan çalışmasını sağlıyor. Üstelik bunun için işletim sistemiyle iletişime geçmeden yapabiliyor.
Tarlox'ta amacım değişkenler gibi dilin temeline yerleşmiş bir şeye paralelizm yükleyerek eşzamanlılığı/paralelizmi varsayılan davranış kılmak ve temel araçlar üzerinden kontrolünü sağlamaktı. Ama tabii işimiz gücümüz olduğundan ve bir preemption mekanizması nasıl işler uğraşmak istemediğimden işletim sisteminin threadlerini kullandım ve "context switch" maliyeti (işte işletim sisteminden programın çalışması arasındaki maliyetten bahsediyorum) aslında biraz performanssız bir şey yapmış oldum. Aslında bunu da bir tercih kabul edebiliriz, prempiton mekanizması çalışma zamanını karmaşıklaştıran bir yapı. Belki de işletim sisteminin işini işletim sistemine bırakmalıyızdır.
Fakat sorun şu ki, neticede elimdeki çözüm kümesi yalnızca bir şeyi zorla var etmek üzerine kurulu. Mesela şu an Tarlox'ta belirsiz kilitlenmeler var. Sebebi kullandığım eş zamanlı HashMap kütüphanesinden kaynaklanıyor. Mesela şu benim epey kafamı karıştıran bir sorundu:
var x = x + 1
Kolay görünüyor değil mi? Tarlox tam da bu noktada kilitleniyordu çünkü x'in değerini ataması için x'e erişebilmesi lazımdı. Kullandığım HashMap kütüphanesi bu tarz durumlarda ya sürekli olarak "müsait mi?" diye sorgulamamı bekliyordu ya da eğer sorgulamadan devam edersem (Spinlock atacak halimiz yok ya... Tamam belki de condvar kullanmaya sıcak bakmalıydım.) doğrudan ortadaki çakışma kalkana kadar beklemeyi tercih ediyordu ki... Bu da mümkün değil bu koşul altında.
Bunun altından kalktım ancak bulduğum çözümün çok da iyi olduğunu söyleyemem ve Tarlox karmaşık senaryolar altında değişkenlerin birbiriyle ilişkilerinde muhakkak bir şekilde kilitlenecek.
Dediğim gibi, burada üzerinde çalışarak uğraşabileceğim bir şeyler olacak ancak her şeyin günü kurtarmalık çirkin çözümlerden ibaret olduğu bir noktaya doğru gidiyoruz bile isteye. Tabii değişkenleri saklamak için kendi HashMap kütüphanemi inşa edebilir, değişkenlerin saklandığı ortamın (environment) erişimi için kuralla Tarlox'u aslında iyilik yapalım derken kullanıcıyı zorlayan, bunaltan bir yapıya dönüşecektirecektim. (Yoo? Rust'a niye laf çakayım? Rust'ı seviyoruz. Arc<Mutex<...)
Tabii Tarlox'un yapmaya çalıştığını yapan programlama dillerine biraz araştırdım ve Chapel'i buldum. Chapel paralelizm konusunda müthiş bir dil, sözdizimi de bana gayet okunaklı göründü fakat buna karşın öğrenmesinin oldukça zor olduğuna kendimce kanaat ettim. Bu yüzden bu işin doğası gereğince karmaşık olduğunu düşünüp kökten geri dönmemek üzere pes ettim.
Bir müddet bu konuya kafa yormadım, çünkü sıkılmıştım ve bir süre ilgimi farklı alanlara verdim. Biraz da bu konunun çözümsüz olduğuna, yani yapılabilecek en iyi şeyin esasında klasik çözümlerden daha garanti ya da daha iyi olmadığına ikna etmiştim kendimi. Fakat kafamda tasarladığım, Tarlox'tan daha düzgün olan paralelizm dili için şöyle kurallar belirlemiştim:
- Dilde kesinlikle mutability (değişebilirlik diye çevirebiliriz) olmamalı. Yeni değerler üretilmeli, sonra da eski değerler bellekten temizlenmeli ya da yazılımcı yeni değerler üretirken arka planda eski değerler basitçe güncellenmeli. (Interior mutability)
- Mevcut durumun bilgisi bir yerde olmamalı. Her şey yeni değerler olarak oluşturulmalı ve fonksiyonlar birbirine yeni durum bilgisini de göndermeli.
- Fonksiyonlar ne olursa olsun yan etki oluşturmamalı.
- Tekrarların çözümü döngüler değil özyineleme olmalı.
Tabii bu koşulların bana Haskell'i hatırlatması tesadüf değil. Tabii saf fonksiyonel mantıktan pek hazzetmediğim için aklımda immutable nesne odaklı bir programlama dili var. Bu uzun vadede hedeflerimden birisi.
Daha sonra Rust gruplarından birisinde HVM'den bahsedildiğini gördüm. Ben ilk incelediğimde ortada HVM üzerine Bend programlama dili yoktu, Haskell'e alternatif bir VM olarak ifade edilmişti. (Haskell... Doğası paralel programlamaya epeyce müsait) Özellikleri benim gibi bir paralel hesaplama amatörünü epey eğlendirecek şeylere sahip: Hesaplama yaparken kendi doğası gereğince paralel hesaplama yapmaya izin veriyor. Mesela şu ifade paralel olarak hesaplanıyor:
(f(x) * g(x)) + (f(y) * g(y))
Burada paralel hesaplama şöyle işliyor: toplama operatörünün iki tarafı öncelikle paralel ayrıca paralel olarak hesaplanabiliyor. Toplamanın operantlarının içindeki çarpım işleminin her tarafı da paralel hesaplanıyor. Böylece toplam dört farklı thread daha biz bir şey demeden ayağa kalkıyor.
Burada şöyle bir şey var, Tarlox değişkenler üzerinde çalışırken aslında belleğe kaydedilecek verileri paralel hesaplarken ve paralel hesaplanan bellek alanlarının erişimi esnasında kilitlenirken, daha da ötesi kilitlenmese bile öngörülemez sonuçlar doğuruyor. Buna karşın HVM ise daha ifadeleri paralel işlenebilecek parçalara ayırarak bu sorunu daha çok önceden ortadan kaldırıyor.
Bunun dışında HVM'in bir bellek kontrolüne ihtiyacı yok. Bir Java'daki gibi çöp toplayıcıya, C'deki gibi bir bellek yönetimine ya da Rust'taki gibi sahiplik modelini gerektirmemesi anlamına geliyor.
Bunu idrak etmek başlangıçta çok zor çünkü HVM sorunun çok çok temeline odaklanıyor. Sorun şu: Yaygın olarak kullanılan hesaplama modeli esasında 1940lı yıllardan kalma Turing makinesi. Çok basit işlemleri yapabilen RiscV işlemciler Ryzen 9 işlemcilere kadar esasında kendi özünde bunu modelliyor. Bugün delikli kağıtlar yerine RAM bellek kullanıyoruz ve transistör teknolojisi ile silikon bu makineyi nanometreler seviyesinde modellememize fırsat tanıyor. Ancak Turing makinesi modelinin bazı açıkları var:
- Kağıtların (siz bellek olarak okuyun) sınırsızlığına dayandığı için, bu kağıtların nasıl efektif olarak kullanılacağına dair bir öngörüde bulunmuyor. Kötü yapılandırılmış talimatlar kağıt israfına sebep olabilir, çünkü özünde hiçbir sınırlama getirmiyor.
- Turing makinesinin kağıtları okuyup tarayacak tek bir başı var. Birden çok Turing makinesi birbiriyle paralel çalışıp, birbirleriyle uyumlu talimatları işleme alabilirler ancak bunun senkronizasyonu talimatları sunan kişiye yani programcıya ek bir yük olarak kalıyor.
Programlamada ve genel olarak bütün mühendislikte sorun bir şeyin yapılamaması değil, o şeyin yapılırken bir bakışta anlaşılır ve basit olmaması. Zeki bir adamın o karmaşa içerisinde bulacağı her bir çözüm, daha sonra o zeki adamın geri döndüğü zaman neyin ne olduğun hatırlaması için ekstra zamana tekabül ediyor. Elde edeceğimiz çözümler matematiğe mümkün olduğunca yakın olmalı ki matematik bilen herkes anlayabilmeli. Tabii bundan bahseden kişi matematiği en son lisede görmüş bir Psikoloji mezunu, onu mazur görünüz.
Bu arada Psikoloji'yi anmışken bir şeyden daha konu açayım. İnsan doğası gereği Psikoloji'de insanları matematiksel bir modelle temsil edemiyoruz. Edebiliyorsak da yaşadığımız toplum Yevgeni Zamyatin'in "Biz" romanındaki gibi entropik bir topluma dönüşmüştür. (Asimov'un kitaplarındaki Psikotarih'i hem sosyolojik bir konu hem de bana gerçekçi gelmiyor.) Bu yüzden her insanla özel bir vaka olarak ilgilenmek gerekir. Bu kendi başına sonu gelmez bir iş yükü demek. Yazılımcılığın buna benzememesine göstermemiz gerekiyor.
Turing makinesi bir insanın nasıl düşündüğünü modelliyor. Bu yüzden matematiksel olmasına karşın tıpkı insan düşünüşü gibi girdi olarak gelen talimatların matematiksel bir doğaya sahip olmasını gerektirmiyor ve her bir "talimat listesinin" (yani programın) tıpkı insanların kendisi gibi baştan keşfedilecek bir dünya olması tehlikesini yaratıyor. Tabii bu bir tez ve biraz da abartıyor olabilirim.
Tarlox'un sorunu burada yatıyor. Paralelizm mantığının özünde anlamlı bir matematiksel model yok. Hesaplama modelini olur da somutlaştırabilseydik aynı kağıdın aynı kısmını sırayla okuyan Turing makineleri görecektik. Bu Turing makinelerinin en basit işlemde birbiriyle tepinip durmamaları için de talimatları hazırlayan kişinin özel hassasiyet göstermesi gerekecekti. Yani biraz daha optimizasyonla bunların tepinip durma ihtimalini azaltabiliriz ancak yine de talimatları sunacak kişinin fazladan efor göstermesi gerekecekti.
HVM her VM gibi (işte JVM olur, Erlang VM olur, şu olur bu olur) bir işlemciyi modelliyor fakat bu sefer bir Turing makinesi değil bambaşka bir hesaplama modelini temsil ediyor. O model ise Interaction Nets. Bu 90lı senelerde ortaya çıkmış bir hesaplama modeli ve bir kağıdı alıp işlemleri modellemek yerine her değeri bir fonksiyonla temsil edip, belli bir yöntemle sadeleştirerek hedef fonksiyon üzerinden yeni değerini gösteriyor. Burada "Lambda Calculus"u aklınıza getirebilirsiniz. Lambda Calculus'te her bir sayının ya da değerin fonksiyonel bir karşılığı vardır. Lambda Calculus'u bizlere armağan eden Alan Church'ün 2 sayısı için kullandığı kodlama şöyle bir şey:
(bir_degerini_veren_fonk, sıfır_degerini_veren_fonk) => bir_degerini_veren_fonk(bir_degerini veren_fonk(sıfır_degerini_veren_fonk())
JavaScript notasyonuyla değil de daha sade olan Lambda notasyonu ile yazarsam şöyle yakışıklı bir şey oluyor:
λx.λy.x x y
Bu arada fonksiyonel programlama geçmişi olmayan dostlar her fonksiyonun bir sonucu olması gerektiğini düşünebilir. Buradaki istediğimiz şey o değere ulaşmak değil o değeri temsil eden bir fonksiyonun elimizde kalması.
Interaction Nets de bu mantıkla hareket ediyor. Tabii bundan daha öte "agent" şeylerle hareket ediyor. Doğrusu anlayabilmiş değilim. Dikkatimi toplayıp içeriğini daha da derin anlamam lazım.
Peki beni heyecanlandıran şey ne? Saf fonksiyonel programlamadan pek hazzetmediğimi söylemiş olabilirim ama "saf fonksiyonel programlamanın" işlemcinin kendisi olması bambaşka bir şey. İşlemcilerin ve bilgisayarların geleceğini de bu hesaplama modelinde görüyorum. Bu mantık üzerine yeni bir işlemci inşa edilebilir ve muhtemelen bu tıpkı GPUlar gibi binlerce çekirdeği olan fakat GPUlardan çok daha farklı işler yapabilen bir işlemci inşa edilebilir. Haskell'in basit versiyonunu andıran bir assembly ile gömülü sistemler tasarlanabilir, hatta hiçbir bellek kontrolü gerektirmeyen gerçek zamanlı sistemler tasarlanabilir.
Bu tabii bir bilişim devrimi de demek ancak bu bilişim devriminin kim tarafından nasıl gerçekleşeceğini de öngörmek zor olacak. Bilgisayarlar neye benzeyecek? Von Neumann stili ortadan kalkarsa ne yapacağız, ne programlayacağız? Bugünün bilgisayarları açısından C ne demekse gelecekte Haskell o anlama mı gelecek? Bunu ön görmek biraz zor.