1 puan yazan GN⁺ 2024-07-22 | 1 yorum | WhatsApp'ta paylaş
  • Rust’taki Pin, async/await’in oluşturduğu Future içinde kendine referans veren durumları güvenli biçimde ele almak için getirilmiş temel bir yapı taşıdır
  • async Future, her await noktasında durumu sakladığı için, bir nesnenin içindeki bir alanın aynı nesnenin başka bir alanına referans verdiği kendine referans veren tip haline gelebilir
  • move constructor, offset pointer ve ?Move tasarımları; sırasıyla çalışma zamanı izleme maliyeti, derlenebilirlik ve mevcut API’lerle geriye dönük uyumluluk sorunları nedeniyle benimsenemedi
  • Nihai tasarım, işaretçiyi sararak hedefi pinned typestate durumuna sokan Pin’dir; Unpin auto trait’i sayesinde çoğu tip eskisi gibi taşınabilir kalır
  • Pin’in zorluğu, değişmezlik kavramının kendisinden çok kütüphane tipi olmasının sınırlarından kaynaklanır; reborrowing, Pin::set, pinned projection ve Drop ile etkileşim kullanılabilirliği ciddi biçimde düşürür

Pin’i gerekli kılan sorun

  • Rust async ekosisteminde Pin ve pinning temel altyapıdır; ancak async Rust öğrenenler için hâlâ zor ve çokça yanlış anlaşılan bir alandır
  • Pin’in amacı, kullanıcıların yalnızca güvenli Rust ile doğrudan kendine referans veren tipler oluşturmasını sağlamak değildir
    • Amaç, derleyicinin async fonksiyonlardan ürettiği kendine referans veren Future’ların veya tokio gibi runtime’ların unsafe kodla oluşturduğu kendine referans veren tiplerin güvenli biçimde işlenmesini sağlamaktır
  • Örnekteki async fn bar, foo(&mut z).await noktasında z’yi ve z’ye referans veren Foo Future’ını aynı Future durumu içinde birlikte saklamak zorundadır
    • Bu durumda Future nesnesinin içindeki bir alan, aynı nesnenin içindeki başka bir alana referans verir
    • Böyle bir Future tipi kendine referans veren tip olur
  • Nesne bu duruma girdikten sonra taşınırsa, iç referans eski bellek konumunu gösterir; bu konum artık ölü bellek olabilir ya da başka bir değer için yeniden kullanılmış olabilir
  • Pin öncesi Rust’ta, bir nesnenin sahipliği veya mutable reference’ı varsa nesne taşınabildiği için, belirli bir andan sonra taşımayı yasaklamayı ifade edecek bir yola ihtiyaç vardı

Çözüm olamayan yaklaşımlar

  • move constructor

    • move constructor, bir değer taşındığında destructor gibi kod çalıştırıp kendine referans veren işaretçileri yeni konuma düzelten bir yöntemdir
    • Rust’ta işaretçiler yalnızca taşınan değerin “içinde” bulunmaz; örneğin kendi durumunu gösteren işaretçilerin bir vektörü içinde de bulunabilir
    • Bu tür işaretçilerin hepsini izlemek için sonuçta çöp toplama benzeri bir çalışma zamanı bellek yönetimi gerekir
    • Rust erken dönemde move constructor’a sahip olmamaya karar verdi ve pek çok unsafe kod, değerlerin yalnızca bellek kopyasıyla taşınabileceği varsayımına dayanır
    • move constructor’ı sonradan eklemek breaking change olurdu
  • offset pointer

    • offset pointer, kendine referansları normal referans yerine kendine referans veren nesnenin adresine göre bir offset olarak derleme yöntemidir
    • Derleme zamanında hangi referansın kendine referans olduğu her zaman belirlenemez
    • Farklı dallarda aynı değer kendi nesnesinin içini de gösterebilir, dışını da gösterebilir
    • Bunu ele almak için referansları offset ve reference’tan oluşan enum benzeri bir biçime derlemek gerekir; async/await çalışmaları sırasında bu gerçekçi olmayan bir yöntem olarak değerlendirildi

pinned typestate gereksinimleri

  • Kendine referans veren Future’ın en baştan itibaren her zaman taşınamaz olması gerekmez; yaşam döngüsünün bir noktasına kadar serbestçe taşınabilmeli, belirli bir noktadan sonra ise taşınmamalıdır
    • Future başka Future’larla birleştirilirken taşınabilir olmalıdır
    • poll edileceği konuma yerleştirildikten sonra artık taşınmamalıdır
  • Ralf Jung’un modeli, mevcut “owned” ve “shared” typestate’lerine ek olarak, kendine referans veren Future için üçüncü bir durum olan pinned typestate’i ekler
  • Bir nesne pinned typestate’e girdiğinde bir daha taşınmamalıdır
    • Daha kesin ifadeyle, önce destructor çalıştırılmadan o nesnenin belleği geçersiz kılınmamalıdır
    • Pratikte bu, nesnenin yeni bir konuma taşınmaması gerektiği anlamına gelir
  • Çoğu tip kendine referans içeremeyeceğinden pinned typestate onlar için özel bir anlam taşımaz
    • Bu tiplerin pinning kısıtlarından çıkıp yeniden taşınabilir olması tercih edilir
  • pinned typestate’in ayrıntılı biçimsel modeli Ralf Jung’un A Formal Look at Pinning yazısında açıklanmıştır

?Move tasarımının başarısız olma nedeni

  • Pin öncesinde Move adlı yeni bir trait’e dayalı tasarım denenmişti
    • Çoğu tip Move’u implement eder
    • Kendine referans içerebilecek tipler Move’u implement etmez
    • Move’u implement etmeyen bir tipin değerine referans oluşturulduğunda, o değer pinned typestate’e girer ve artık taşınamaz hale gelir
  • Bu yaklaşım, referans oluşturma anı ile pinning geçişini ilişkilendirerek güvenliği sağlaması bakımından sezgiseldi
    • Hatta gerçekten derleyicinin bir branch’inde implement edilmişti
  • Temel sınır, ileride kendine referans verecek bir değere kısa süreliğine referans almak ama henüz pinning yapmak istememek gibi durumların bulunmasıydı
    • Örneğin bir değeri kısa süreliğine Option içinde saklayıp sonra Option::take ile çıkarmak isteyebilirsiniz
  • Daha büyük sorun geriye dönük uyumluluktu
    • Move bir auto trait yapılamıyordu
    • Çünkü mem::swap gibi, mutable reference üzerinden bir değerin her zaman taşınabileceğini varsayan stable API’ler zaten vardı
  • ?Move olarak ekleme yöntemi de associated type nedeniyle geriye dönük uyumlu değildi
    • Bir trait’in associated type’ına ?Trait bound’u ekleme yeri trait tanımıdır
    • Mevcut bir trait’in associated type bound’unu gevşetmek, o bound’a dayanan kodları bozabilir
    • IntoFuture’ın associated future type’ı, DerefMut’ın Target’ı, fonksiyon dönüş tipleri, iterator item’ları, index operator dönüş değerleri, aritmetik operator dönüş değerleri gibi pek çok temel işlem associated type’larla iç içedir
  • edition ile de kolayca çözülemiyordu
    • Çünkü farklı edition’lardaki crate’lerin birlikte kullanılabilmesi için trait arayüzlerinin aynı kalması gerekir

Pin tasarımı

  • Nihai tasarım, pinned typestate’i nesne tipinin bir özelliği olarak değil, özel bir işaretçinin oluşturduğu durum olarak ifade eder
  • Pin, bir işaretçiyi saran wrapper type’tır
    • built-in reference type’ları da sarabilir
    • Box gibi kütüphane tanımlı smart pointer’ları da sarabilir
  • Pin, ilgili işaretçinin gösterdiği hedefi pinned typestate’e sokar ve hedef artık taşınmamalıdır
  • Değişikliği en aza indirmek için bu tasarım, derleyici özelliği olarak değil kütüphane API’si olarak implement edilmiştir
    • pinned nesneyi gerçekten değiştirmesi gereken kod, unsafe API üzerinden erişmelidir
    • Bu sırada, normal mutable reference üzerinden nesnenin taşınmayacağı garantisini sağlamalıdır
  • Çoğu tip için pinned durum ile normal durum arasında anlamlı bir fark olmadığından Unpin auto trait’i eklenmiştir
    • Bir tip kendine referanslı olamıyorsa, pinned pointer’dan unsafe olmadan mutable reference alınabilir
    • Unpin implement eden bir nesneyi Pin dışına taşımak güvenlidir
  • pinning yalnızca pinned pointer’a uygulandığı için, normal unpinned reference’lar Unpin olmayan tiplerle de çalışmaya devam eder
  • Ek açıklamalar standart dokümantasyondaki Pin tipi ve pin modülünde yer alır
  • Bu tasarımın en büyük avantajı, mevcut kodu bozmadan eklenebilmiş olmasıdır
    • swap gibi referanslanan veriyi taşıyabilen API’ler mutable reference gerektirir
    • Bir nesneyi Pin ile pinning yaptığınızda, bu tür API’ler artık o nesne üzerinde çağrılamaz
    • pinned typestate yalnızca özel pinned reference’a uygulandığı için Rust dilinin genel geriye dönük uyumluluk garantisini bozmaz

Pin’in kullanılabilirlik sorunları

  • Pin, gereksinimleri geriye dönük uyumlu biçimde karşıladı; ancak kullanıcı doğrudan ele almaya başladığı anda bir karmaşıklık uçurumu oluşur
  • Bir açıklama, pinned nesneyi değiştirmek için unsafe kod gerekmesidir
    • Ancak bu sorun abartılmamalıdır
    • Pin::set ile pinned nesneye güvenli biçimde atama yapılabilir
    • Gerçekte pinned nesneyi değiştirmesi gereken kod çoğunlukla async fonksiyonları Future’a indirgeyen derleyici üretimli koddur; kullanıcıların doğrudan yazması nadirdir
  • Pin’in koşullu çalıştığı için zor olduğu açıklaması da temel neden değildir
    • Rust’ta koşullara göre farklı davranırken anlaşılmayı kolaylaştıran özellikler vardır
    • non-lexical lifetimes, koşul dallarına göre lifetime’ın farklı noktalarda bitmesini sağlayan bir örnektir
  • Temel sorun, Pin’in saf bir kütüphane tipi olmasına karşılık, normal reference type’ların dilin yerleşik tipleri olması ve çeşitli sözdizimi desteği ile sugar almasıdır
    • Normal referanslarda doğal biçimde çalışan özellikler pinned reference’larda kaybolur
    • Kullanıcının derleyicinin kabul ettiği referans davranışlarına göre kurduğu mental model, pinned reference’larda bozulur

reborrowing ve Pin::as_mut

  • Normal mutable reference &mut T, Copy implement etmez ama aynı argüman olarak birden çok kez geçirilebilir
    • Çünkü derleyici, x yerine &mut *x koymuş gibi örtük biçimde reborrowing yapar
  • Pin<&mut T> normal bir kütüphane tipidir ve Copy implement etmediği için bu kolaylık yoktur
    • Pin<&mut T> iki veya daha fazla kez kullanılırsa move sonrası değer kullanımı hatası ya da daha anlaşılması zor lifetime hataları oluşabilir
    • reborrow yapmak için açıkça Pin::as_mut çağrılmalıdır
  • Normal mutable reference’larda dereference ve assignment operator ile doğrudan atama yapılabilir; Pin’de ise set metodunu öğrenmek gerekir
    • Bu tür özel API’lerin artmasının nedeni, Pin’in dil sözdizimi desteği olmayan bir kütüphane tipi olmasıdır

pinned projection ve Drop

  • pinned projection, bir nesneye ait pinned reference’tan o nesnenin alanına ait pinned reference elde etme problemidir
    • projection, nesneden alana erişmek anlamına gelir
  • Normal referansta alan erişiminden çok daha zor olduğu için pin-project-lite gibi üçüncü taraf crate’ler kullanılır
    • Bu tür crate’ler, macro dahil karmaşık yeni API’lerin öğrenilmesini gerektirir
  • En kötü etkileşim pinned projection ile Drop trait’i arasında ortaya çıkar
    • Drop::drop normal mutable reference alır
    • Bir tipte kendine referans veren bir alan varsa ve o alana pin project yapılıp poll edildikten sonra destructor içinde o alan taşınırsa, pinning garantisi bozulabilir
    • Örneğin destructor içinde o future stack üzerinde pinning edilip poll edilirse, mevcut pinning garantisi ihlal edilir
  • pin-project-lite gibi crate’ler, destructor tanımlama yeteneğini kısıtlayarak bu sorunu ele alır
    • Pratikte işe yarar; ancak pinning garantilerini açıklarken dokümante edilmesi gereken karmaşıklık ekler
    • Drop, Pin’den önce stable olduğu için bir dolambaçlı çözüme ihtiyaç vardı

Mevcut değerlendirme ve sonraki iyileştirme yönü

  • Pin, rastgele referanslar içeren async fonksiyonların güvenli kendine referans veren nesnelere derlenmesini mümkün kıldı
    • Referanslar, Rust kullanıcılarının kod yazma biçiminin önemli bir parçası olduğundan, bu olmasaydı async/await’in kullanılabilirliği büyük ölçüde düşerdi
  • Aynı zamanda Pin, mevcut Rust ile tamamen geriye dönük uyumlu bir biçimde eklendi
  • Pin, yüksek performanslı ağ servislerini ve asenkron programlamanın diğer kullanım senaryolarını destekleyen ekosistemin temel yapı taşı haline geldi
  • Ancak pinned reference’larla çalışmak ordinary reference’larla çalışmaktan çok daha zordur ve Pin gerçekten bir karmaşıklık uçurumu yaratır
  • Sonraki iyileştirme yönünün temel kavramı pinned places’tır

1 yorum

 
GN⁺ 2024-07-22
Hacker News yorumları
  • Pin’in resmi belgelerde net açıklanmadığı için anlaşılmasının zor olduğunu hep düşünmüşümdür
    Özellikle “Pin, bir nesnenin asla taşınmamasını garanti eder” gibi çok açıklama var ama bu doğru değil
    Yalnızca nesne Unpin olmadığında doğru; sıradan nesnelerin çoğu Unpin olduğu için Pin genelde hiçbir şey yapmaz
    Bunu anlamam çok uzun sürdü; Pin’in gerçekten anlam taşıdığı T tipleri kümesi epey özel ve tuhaf, belgelerin de bunu yeterince vurgulamadığını düşünüyorum

    • İyi bir geri bildirim; belgelerin bu kısmı daha netleştirmesi iyi olurdu
      Elbette pratikte sabitlenmiş durumda ele alınacak tipler olan future’lar ve stream’lerin bu tür özel nesneler olma olasılığı çok daha yüksek
      Yine de belgelerin son birkaç yılda çok daha iyi hale geldiğini düşünüyorum
      Bu yazıyı yazarken kontrol ettiğimde, oldukça yerinde noktalara odaklandığını görünce şaşırdım; 2019 civarında ise bunun std API belgelerinden çok Rust referans belgelerine girecek türden sözleşme tanımlarına çok daha fazla kaydığını hatırlıyorum
  • Kullanıcıların Pin’i zor bulmasının nedeni bence Pin’in tek başına bir anlamının olmaması
    Bu, dildeki diğer sarmalayıcılardan farklı; istisna olarak da asıl kullanım amacıyla neredeyse kimsenin kullanmadığı AssertUnwindSafe sayılabilir
    Elinizde Pin<&mut InnerType> olduğunda, dilin ya da standart kütüphanenin içindeki Pin’in kendisi, ne yapıp ne yapamayacağınızı söyleyen bir şey sunmaz
    Yalnızca InnerType Unpin olarak bildirilmişse, normal bir işaretçiyle yapılabilecek her şeyin yapılabileceği anlamına gelir
    Bunun yerine Pin, “anlamı kendin getir” şeklinde çalışır; InnerType sağlayıcısının, sabitlenmiş nesneyi güvenli şekilde manipüle etmek için içeride unsafe olan ek metotlar ve API’ler oluşturduğu bir yapı söz konusudur
    Pin’in kendi amacı, &mut değiştirme, Box içinden çıkarıp taşıma gibi daha az içkin yeteneğe sahip bir işaretçi sağlayarak, iç tipin bunun üzerinde ek yetenekleri güvenli biçimde izinli kılabilmesini sağlamaktır
    İnsanları en çok bu anlam belirsizliğinin kafasını karıştırdığını düşünüyorum; benim de anlamam epey uzun sürdü
    Yapısal alanlar ve yapısal olmayan alanlar kavramı ise yalnızca “bu alan sıradan veri, ama şu alan kendi başına sabitlenmek isteyen bir nesne içeriyor” gibi yaygın erişim desenlerini mümkün kılmak için kullanılan bir düzenektir

    • Pin’in bir anlamı var. Hedef tip Unpin uygulamadığı sürece, bu işaretçinin hedefi bir daha taşınamaz demektir
      Daha kesin ifadeyle, yıkıcısını çalıştırmadan hedefi geçersiz kılamazsınız demektir; taşımanın sorun olmasının nedeni de budur
      Belirli haklardan vazgeçtiğinizde, öz-gönderimli değerler saklama hakkı gibi başka haklar elde edersiniz
      Bileşenler arası sözleşmeler genelde bu şekilde işler
      Benzer biçimde, bir referans üzerinden değiştirme hakkından vazgeçerseniz, aynı anda o referansı alias’lı hale getirebilirsiniz
      Bunu her düşündüğümde, konu tamamen farklı ve çok daha ağır olsa da Lincoln filmindeki şu replik aklıma geliyor: “Yasalara itaat edersek, Alex, özgürlüğümüzü kaybetmeye kadar itaat edersek — örneğin baskı kurma özgürlüğünü — daha önce bilmediğimiz başka özgürlükler keşfedebiliriz”
      Ancak güvenli kodda bu hakları doğrudan kullanamamanın eğitim açısından sorun olduğu konusunda katılıyorum
      Çünkü sabitlenmiş bir referansla ne yapılabileceğini, “derleyicinin oluşturduğu poll metodunu çağırmak” dışında kolayca göstermek zor
  • Rust ile birkaç yıldır profesyonel olarak geliştirme yapıyorum ama açıkçası Pin’i o kadar iyi anlamıyorum
    Teoriyi biliyorum, ama ne zaman kullanılması gerektiğine dair pek sezgim yok
    Pin kullanımı fiilen “bir şey denedim, derleyici şikâyet etti; şunu bunu sabitleyince derlendi” durumuna daha yakın
    Günlük kodlamada gerçekten oturup derinlemesine anlamam gerekecek kadar bir engel haline henüz gelmedi

    • Ben de aynıyım. “Sadece unsafeten kaçın ve zeki derleyici insanlarının bunları zaten çözdüğüne şükret” kategorisindeki en yaygın durumlardan biri
      Buna karşılık C++’ta “bilmiyorum ama mutlaka kullanmam gereken şeyler”in kıyısında sık sık yürürken timsahlar tarafından yenirdim
  • Öğretirken Unpin öğelerinin Pin’den etkilenmediğini netleştirmek için, yerinde tutmak üzere yapılmış bir aracın bile etki etmediği gerçek dünya benzetmeleri kullanmak iyi olabilir
    Cırt cırt kancaları pürüzsüz yüzeye tutunmaz: Pin → cırt cırt, Unpin → pürüzsüz yüzey
    Mıknatıs manyetik olmayan maddeleri etkilemez: Pin → mıknatıs, Unpin → manyetik olmayan/cam/pirinç
    Yapıştırıcı yapışmaz yüzeye tutunmaz: Pin → yapıştırıcı, Unpin → yapışmaz yüzey
    Böylece “cırt cırt”ın nesneyi yerinde sabitlediği, ama nesne “pürüzsüzse” cırt cırt mekanizmasından etkilenmediği netleşir
    Rust ekosistemindeki adlandırma havasını düşününce, trait adlarını mıknatıs ve manyetik olmayan gibi bir yöne değiştirmiş olsalardı güzel olurmuş

    • Ama pürüzsüz bir nesneyi cırt cırtla tutturamazsınız; ahşap da mıknatısı tutamaz
      Unpin, nesnenin her an sabitlenmeye hazır olduğu anlamına gelmiyor mu diye düşünüyorum
      Dün gece yazıyı okudum ama sabitleme için bir düzeltme adımı gerekip gerekmediğini şimdiden unuttum
      Bu yüzden T: Pin + !Unpin yalnızca zımbayla sabitlenebilen bir kâğıt gibi; T: Pin + Unpin ise halkası olan bir tabloya, çiviye asılıp halkasını bozmadan tekrar indirilebilen bir şeye daha yakın bence
  • “Değer kimliği” terimi bu yazının hiçbir yerinde tanımlanmıyor ve Mojo belgelerinde de bulamadım; bu yüzden Modular’ın, Mojo’nun Pin’in çözmeye çalıştığı sorunu çözdüğünü hangi temele dayanarak söylediği net değil
    Ben de yanıtı bildiğimi iddia etmiyorum ama Chris Lattner ile birlikte Swift’in değer semantiği üzerinde çalışmış Dave Abrahams’ın mükemmel bir sunumu aklıma geliyor
    Sunumun başlığı “Value Semantics: Safety, Independence, Projection, & Future of Programming”
    [0] https://www.youtube.com/watch?v=QthAU-t3PQ4

    • Mojo’nun bir anlamda Swift’in değer semantiği kavramını miras aldığı açık, ancak Rust da aynı anlamda değer semantiğine sahip
      Rust, referansları da birinci sınıf tip olarak barındırırken, Swift ve bana göre Mojo referanslara yalnızca parametre geçirme biçimi olarak izin veriyor
      Mojo, Swift’in inout parametresini genişleterek değişmez referansla geçirme biçimini de eklemiş gibi görünüyor
      Nesnelerin içinde referans saklanmasını engellerseniz Rust’ın derlediği türden kodları uygulayamazsınız; bu yüzden “kendine referans veren struct” sorununu çözmüş olursunuz
      Ancak alıntılanan paragrafın Mojo hakkında söylediği şey hiç de bu değil; bu yüzden ne demek istediği epey kafa karıştırıcı
  • Bana göre sorun, bir değere yönelik &mut referansınız varsa mem::swap/replace gibi şeylerle o değeri taşıyabilmeniz
    Ama pratikte bunu yapmanız gereken durumlar nadirdir
    Buna izin verilmeseydi kendine referans veren değerler için &mut referans almak tamamen güvenli olurdu gibi geliyor
    Belki de yalnızca gerektiğinde referans üzerinden taşımayı açıkça seçmenin bir yolu olabilirdi; swap ve replace unsafe yapılsaydı bu sorunun tamamından kaçınılabilirdi
    Keşke biri bu tasarım alanını araştırsa

    • Doğru. Bu sorun üzerinde çalışıldığı dönemde Aaron Turon, &mut’un fazla güçlü olduğunu söylemişti
      &mut içindeki değeri taşıma yetkisi vermeseydi tüm tasarım çok daha basit olurdu
      Bir sonraki yazıda bunu ele alacağım
      Rust geriye dönük uyumluluğu korumak zorunda ve &mut üzerinden değer taşınabileceğine zaten karar verdi; ancak geçmiş kararlara bağlı kalınmasa çok daha temiz bir tasarımın mümkün olduğu kesin
    • Bu doğru ama ölçeklenebilir değil. Mevcut kodların çok fazlasını bozacağı için en baştan uygulanabilir olması zordu
      mem::swap, değiştirilebilir referans üzerinden değer taşımanın yollarından yalnızca biri; başka pek çok yol da var
      Option::take benim oldukça sık kullandığım bir örnek; bunun unsafe olması gerçekten tuhaf olurdu
  • Bu arka plan hikâyesini görmek güzel. WithoutBoats zaten çok yerinde bir zamanda asenkron yineleyiciler, poll, pin konularında çok sayıda aktif tartışma yürüttü
    https://news.ycombinator.com/from?site=without.boats
    Bir dilin ince iç ayrıntılarına bu kadar derinden ve açık biçimde giren çok fazla topluluk yok gibi; izlemesi de çok keyifli

    • Harika ama bu aynı zamanda dil geliştirme sürecinin çok yavaş olduğu anlamına da geliyor
      Async hâlâ yarı pişmiş durumda ve çok karmaşık
      Bunu son 3 yıldır haftada 40 saat Rust kodu yazan biri olarak söylüyorum
  • Taşıma kurucusu olan, oluşturulan tüm Future alt tiplerinin opak olduğu ve otomatik olarak heap’e ayrıldığı Rust benzeri bir dil hayal edilebilir
    O zaman kullanıcının onu bozmasının bir yolu olmaz; opak olduğu ve heap’in başka bir yerinde bulunduğu için taşınmasının da yolu olmaz, böylece Pin’e gerek kalmayabilir
    Çünkü taşıma kurucusunun olması, taşımanın kavramsal olarak yıkımdan sonra yeniden oluşturma anlamına gelmesidir

    • Pin, verinin kendisinin bir özelliğinden çok bir durumdur
      Bu sayede, çalıştırmadan önce Future’ları birleştirip inline hâle getirebilmek gibi iyi bir etkisi var
      Rust’taki değişmezliğe de benzer. Değişmez bellek yoktur; yalnızca değişmez referanslar vardır
    • Tüm future’lar heap’e ayrılmış olsaydı taşıma kurucusuna gerek olmazdı
      Ama bu durumda her asenkron fonksiyon çağrısında ayrı bir ayırma oluşur ve bu bellek yerelliği açısından çok kötüdür
      Bir tür sanal stack bundan çok daha iyi olurdu; ancak stack’i varsayılan olarak küçük olacak şekilde optimize etmek için eninde sonunda garbage collection gerekir
    • Ne kadar yıkıcı bir değişiklik olacağını tahmin ediyorum; yine de Rust’ın bunu doğrudan benimseyip Copy ile benzer düzeyde dile gömülü bir Move trait’ini std’ye eklemesini gerçekten isterdim
      Move, bir değeri bellekteki bir adresten başka bir adrese taşıyan fonksiyonu tanımlar ve impl Move olmayan struct’ların taşınamamasını sağlar
      Neredeyse tüm tipler #[derive(Move)] ekler; bu da byte’ları kopyalayan basit bir taşıma fonksiyonunu implement etmekle yetinir
      Ama böyle olursa öz-referanslı tiplerin, future’ların ve daha karmaşık taşıma davranışı gerektiren pek çok şeyin önü açılır
      Gerçekte, Copy ile Clone arasındaki farkı yansıtacak şekilde bunu iki trait’e ayırmak daha mantıklı olabilir
      Biri, byte’ları olduğu gibi taşımanın sorun olmadığını derleyiciye bildiren bir marker trait; diğeri de kullanıcı tanımlı “taşıma kurucusu” implementasyonlarına izin veren bir yapı olurdu
      Pin’i anlamak çok zor olduğu için Move’un olmasını isterdim
      Karmaşık kavramlar çift olumsuzluklarla, bazen de üçlü olumsuzluklarla sarılmış durumda. fn(...) gibi bir şey görünce “bu ne?” diyorsunuz; unsafe pin projection noktasına gelince de kopuyorsunuz
      Ne zaman güvenli, ne zaman güvensiz olduğunu bilmiyorum ve sadece uzak duruyorum
      Move olmayan Rust’tan Move olan Rust’a geçmek rahatsız edici olurdu
      Şimdiye kadar yazılmış neredeyse tüm struct’lara #[derive(Move)] eklemek gerekir; std için de aynı şey geçerli
      Eski edition’larda sabitlenmemiş tüm tipler için derleyicinin Move trait implementasyonunu çıkarsaması gerekir
      Mekanik olarak mümkün olurdu, sadece çok iş çıkarırdı
      Asenkron Rust berbat. Özellikle de neredeyse diğer tüm dillerdeki future/promise yapılarıyla karşılaştırıldığında
      Bir gün birileri Rust’ın bellek güvenliği modelini iyileştirip Move trait’i ve daha iyi future’ları olan yeni, Rust benzeri bir sistem dili yapacak
      Kişisel olarak Rust’ın makro sistemi yerine derleme zamanı yürütme de olmasını isterdim
      Rust’ı seviyorum; ekibin yıllardır yaptığı tüm işleri de seviyorum
      Ama gerçekten beklediğim dil, Rust’tan sonra gelecek dil
      Aynı fikirleri taşıyan ama Rust’ın hatalarından öğrenmiş bir dil; böyle daha iyi bir Rust tarzı dilin nasıl görüneceği giderek daha netleşiyor
      Gerçekten sabırsızlıkla bekliyorum
  • WithoutBoats’un bir başka harika yazısı
    Dürüst olmak gerekirse bu, Rust’ta asenkron runtime’ın içinde soyutlanıp gömülü kaldığı için iyi ki öyle dediğim noktalardan biri
    Yine de özel Future implementasyonları dışında Pin’in gerçekte nerede kullanıldığını merak ediyorum

    • Ben de merak ediyorum
      İfade biçimine bakınca FFI içinde kullanılabilecekmiş gibi duruyor
      Örneğin bir extern fonksiyon *mut T döndürüp bir pointer alıyorsa, bunu Pin<&mut T> ile sararak daha iyi semantik verilebilirmiş gibi görünüyor
      Ancak yazıda “sabitlenmiş tip durumu hakkındaki bir başka gerçek de, çoğu tip için tamamen alakasız olmasıdır. Bir tipin değeri asla öz-referans içeremiyorsa, onu sabitlemenin faydası yoktur” deniyor
      FFI konusunda hâlâ çok acemiyim; bunu güvenli Rust ile sarmalamanın en iyi yolunu anlamak istiyorum
    • FFI’de, C API’sinin öğeleri referans olarak değil pointer olarak dışa açtığı ve bu yüzden taşınmamaları gereken durumlar var
      Adrese bağımlı sistem tipleriyle etkileşimde de bu geçerli
      Örneğin bazı işletim sistemlerindeki mutex/futex yapılarında kernel dokümantasyonu kullanıcı alanındaki kilit nesnesinin ilklendirildikten sonra adresinin değişmemesi gerektiğini söylediği için, bildiğim kadarıyla std Pin’e denk bir şey kullanıyor
      Kilitli değilken bile adresin değişmemesi gerektiği nokta ilginç
      Genelde koşul yalnızca kilitliyken geçerlidir; bu durumda da referans verilen hedef taşınamayacağı için Pin’e gerek olmaz
  • Asıl sorun olan iş parçacıklarının verimsizliğini düzeltmemek için muazzam bir iş yapılıyormuş gibi görünüyor
    Tüm asenkron kod, istisnasız, durum yönetimi için bolca sözdizimsel şekerle hafif iş parçacıkları uygulayan bir hack’tir
    Rust gibi dillerde, aslında hiç gerek olmayacak karmaşıklığı inanılmaz ölçüde artırır
    İş parçacıklarının verimlilik ve ölçeklenebilirlik sorunları düzeltilirse bütün bunlar ortadan kalkar
    Puf diye yok olur
    Java gibi dillerde nullın “trilyon dolarlık hata” olmasına benzer
    Tek bir tasarım kararı, ya da burada tasarımın yokluğu, muazzam bir karmaşıklık doğuruyor

    • İş parçacıkları makul bir iptal mekanizmasını desteklemez
      İptal, ağ uygulamaları ve GUI’lerde çok yararlıdır
      İş parçacıkları hem CPU’yu hem ağı yeterince kullanıp ikisinden hiçbirini aşırı işgal etmeyecek şekilde uygulama geliştirmeyi zorlaştırır
      İşleri iş parçacığı havuzları arasında devretmeye başladığınızda future’ları yeniden uygulama yoluna girmiş olursunuz
      Ya da callback/event’lerle çalışırsınız; bu da kodu parçalara ayırır ve async/await tam da bunun sözdizimsel şekeri olmaya çalışıyordu
      İptal ve timeout’un alternatifi, Go’daki gibi Context nesnesini tüm koda örmektir; bu da uçtaki kodun Contexte düzgün uymayan fonksiyonları safça çağırması sorununu doğurur
      Bu, asenkron kod içindeki asenkron olmayan fonksiyon sorunundan yalnızca çok az daha iyidir
    • “İş parçacıklarının verimlilik ve ölçeklenebilirlik sorunlarını düzeltmek” için Linux çekirdeğini yeniden yazmanın mümkün olduğundan şüpheliyim; mümkün olsa bile Pin’in çalışmasını sağlayan Rust uzmanları ile böyle bir işi yapabilecek çekirdek uzmanları kümesi muhtemelen aynı kişiler değildir
      Bu yüzden somut olarak ne yapılması gerektiğini merak ediyorum
      Sadece ellerini kaldırıp “Bir gün biri Linux’u düzeltip iş parçacıklarını sihirli biçimde hızlı hale getirebilir, o yüzden dilimize asenkron eklemeyeceğiz” mi demeliydiler?
    • Eşzamanlı süreçlerle senkronize olan fonksiyonları olmayanlardan ayırt edip işaretlemek aslında iyi bir şeydir
    • Ne yazık ki kullanıcı alanı sınırını aşmanın, “iş parçacıkları” ne kadar hafif olursa olsun, bir maliyeti vardır
      Ayrıca işletim sistemini tüm asenkron işlerin zamanlayıcısı haline getirirseniz, tüm runtime’lar işletim sistemi zamanlayıcısını kullanmak zorunda kalacağından çeşitli zamanlayıcı tasarımları imkânsız hale gelir
    • Rust’ın aldığı zararlı kararlar, önceki hataları ne pahasına olursa olsun sürdürme kültürünün derine işlemiş olduğunu gösteriyor
      “Tercih edilen yaklaşımın” uygulanamaz olduğu ortaya çıktıktan sonra bile maliyet/fayda değerlendirmesini yeniden yapmıyor gibi görünüyor
      “X özelliğini istiyorum, sonuçları umurumda değil” yaklaşımı dil tasarımında pek nadiren kazanan bir hamle olur