İçeriğe geç
academia.sh

Ders 03 / 12

Alan Özel Diller

Aynı yönlendirme yapısı bir makro yazımıyla ve elle kurulan yazımla ayrı ayrı oluşturulur, aynı sonucu verdiği koşturularak gösterilir ve makro yazımının kaynağı kaç satır kısalttığı sayılır; ama makro çıktısının okunaklı olması doğruluk getirmez — mantıksal olarak yanlış bir yol-işlev bağı da aynı kolaylıkla tip denetiminden geçer.

Yayın Güncelleme

Bu derste

  • Aynı yönlendirme yapısını bir makro yazımıyla ve elle kurulan yazımla oluşturup aynı sonucu verdiğini koşturarak gösterir
  • Makro yazımının aynı işi kaç satır kısalttığını, kaynağın kendi metnini sayarak ölçer
  • Okunaklı bir makro yazımının doğruluk garantisi getirmediğini, mantıksal olarak yanlış bir yol-işlev bağının da tip denetiminden geçtiğini örnekle gösterir
İçindekiler

Önceki iki ders, üretilen bir gövdenin nasıl denetlendiğini iki farklı kurguda gösterdi: bir kez aynı okumada metinsel yer değiştirmeyle, bir kez ayrı bir derleme adımında çalıştırılan bir programla. İkisinde de ortak bir soru henüz sorulmadı: üretim neden tercih ediliyor, elle yazmak yerine? Bu konunun son dersi bu soruyu ele alıyor. Bir makro, çağıranın gözünden bir küçük dil gibi görünen bir arayüz kurabilir — kendi sözdizimiyle, kendi kısayollarıyla; buna alan özel dil deniyor. Bu ders aynı yapıyı iki yazımla kuruyor, ikisinin aynı sonucu verdiğini koşturarak doğruluyor, makro yazımının kaç satır kısalttığını sayıyor ve sonunda bu kısalmanın bir doğruluk garantisi taşımadığını gösteriyor.

Alan özel dil terimi burada geniş bir çerçeveyi değil, dar ve somut bir kullanımı işaret ediyor: kaynağı okuyan tarafın, altında yatan match bloğunu, işlev tanımını ya da başka bir mekaniği hiç görmeden, doğrudan problem alanının kelimeleriyle (bir yol, bir işleyici) yazı yazabilmesi. Bu arayüz derleyicinin yeni bir kural öğrenmesini gerektirmiyor — makro genişledikten sonra ortaya çıkan kaynak, önceki iki derste görüldüğü gibi, sıradan Rust kaynağından ayırt edilemiyor. Kazanılan şey yalnız kaynağı yazma biçimi; kaynağı denetleme biçimi hiç değişmiyor.

Bu tanım, alan özel dil terimini programlama dilleri kuramındaki geniş kullanımından ayırıyor: orada terim çoğunlukla kendi ayrıştırıcısı, kendi çalışma zamanı olan bağımsız bir dili işaret ediyor. Burada ise ortada ikinci bir dil yok — tek bir dil var, Rust’ın kendisi ve makro yalnız o dilin sözdizimini belirli bir problem için daha kısa bir kalıba sığdırıyor. Bu yüzden derste kurulan arayüz, kendi başına bir ayrıştırıcı taşımıyor; ayrıştırma zaten derleyicinin kendi ayrıştırıcısı tarafından, makro genişlemesinin bir parçası olarak yapılıyor.

Aynı Yapı, İki Yazım

Bir yönlendirme tablosu — bir yol dizgisini bir işleve bağlayan bir tablo — hem bir makro yazımıyla hem elle kurulabilir. Makro yazımı, yol ve işleyici çiftlerini bir liste gibi sıralar; elle yazım aynı tabloyu bir match bloğunun dallarına döker. İkisi de aynı işi yapıyor: bir yol dizgisini bir sonuca çeviriyor.

Elle yazılan biçim her yeni yol için üç şeyi tekrar etmeyi gerektiriyor: match bloğunun kendisini, bir dalın sözdizimini ve yakalayıcı dalın varlığını. Bu tekrar bir hata değil — Desen Eşleme dersinde (M10/K01) görüldüğü gibi, match’in bütünlük istemesi kaynağın güvenli kalmasını sağlıyor. Ama tekrar eden kısım, yol ile işleyici çiftinden başka hiçbir yeni bilgi taşımıyor: match, yol {, _ => -1 satırları her yönlendirme tablosunda birebir aynı. Makro bu sabit kalıbı bir kez, kendi tanımının içinde yazıyor ve her çağrıda yeniden üretiyor; çağıran taraf yalnız değişen kısmı — çiftlerin kendisini — yazıyor.

// yonlendirici.rs — ayni yonlendirme tablosu bir makro yazimiyla ve elle kurulan esdegeriyle olusturuluyor.
macro_rules! yonlendirici_olustur {
    ($ad:ident { $($yol:literal => $isleyici:expr),+ $(,)? }) => {
        fn $ad(yol: &str) -> i32 {
            match yol {
                $($yol => $isleyici,)+
                _ => -1,
            }
        }
    };
}

fn ana_sayfa() -> i32 { 200 }
fn hakkinda_sayfa() -> i32 { 200 }

yonlendirici_olustur!(makro_ile_yonlendir {
    "/ana" => ana_sayfa(),
    "/hakkinda" => hakkinda_sayfa(),
});

fn elle_yonlendir(yol: &str) -> i32 {
    match yol {
        "/ana" => ana_sayfa(),
        "/hakkinda" => hakkinda_sayfa(),
        _ => -1,
    }
}

fn main() {
    for yol in ["/ana", "/hakkinda", "/bilinmeyen"] {
        let m = makro_ile_yonlendir(yol);
        let e = elle_yonlendir(yol);
        println!("{:<12}makro={} elle={} ayni_mi={}", yol, m, e, m == e);
    }
}
/ana        makro=200 elle=200 ayni_mi=true
/hakkinda   makro=200 elle=200 ayni_mi=true
/bilinmeyen makro=-1 elle=-1 ayni_mi=true

MK7. Makro tanımının deseni üç parçadan kuruluyor: $ad:ident üretilecek işlevin adını yakalıyor, $yol:literal => $isleyici:expr çiftini bir ya da daha çok kez (+) tekrarlıyor, $(,)? de son çiftten sonra isteğe bağlı bir virgülü kabul ediyor. Bu üç parça birlikte, dilin kendi sözdizimini bir liste-benzeri yazıma dönüştürüyor: çağıran taraf match yazmıyor, süslü parantez içine yol-işlev çiftlerini art arda diziyor. Üç yoldan (/ana, /hakkinda, /bilinmeyen) üçü de iki yazımda birebir aynı sonucu veriyor; ayni_mi sütunu bunu tek tek doğruluyor.

Bu ifade edici arayüz bildirimsel bir makroyla kuruldu, prosedürel bir makroyla değil — bu seçim rastgele değil. Önceki dersin gösterdiği gibi, prosedürel bir makro ayrı bir kasada derlenmek zorunda ve onu kullanmak iki aşamalı bir derleme gerektiriyor. Burada kurulan yönlendirme tablosu, tekrar eden bir sözdizim kalıbından ibaret; bu tür bir tekrarı sarmak için andaç akışını elle işlemeye, ayrı bir kasaya ya da iki aşamalı bir derlemeye gerek yok — bildirimsel makronun desen eşleme düzeneği zaten yeterli. Prosedürel makro, girdi üzerinde gerçek bir hesaplama yapmak gerektiğinde tercih ediliyor; yalnız bir kalıbı tekrarlamak için değil.

Üretilen makro_ile_yonlendir işlevi ile elle yazılan elle_yonlendir işlevi, derleyicinin gözünde aynı statüde: ikisi de birer fn, ikisi de aynı imzayı taşıyor, ikisi de aynı match mantığını çalıştırıyor. Bu, önceki iki dersin bulgusunu bir kez daha doğruluyor — üretilen gövde “sürdürüyor” sınıfına düşüyor — ama bu derste asıl soru gövdenin denetlenip denetlenmediği değil, gövdeyi yazmanın ne kadar kısaldığı.

Bu eşitliğin bir sonucu var: iki yazım arasında seçim yaparken, çalışma zamanı davranışı hiçbir zaman bir ölçüt olamaz — ikisi de birebir aynı şeyi üretiyor. Seçimi belirleyen tek eksen kaynağın nasıl okunduğu ve nasıl yazıldığı. Yol sayısı arttıkça bu fark daha belirgin hale geliyor: üç yol yerine otuz yol eklemek gerekseydi, elle yazım otuz kez match dalının sözdizimini tekrar ederdi, makro yazımı yalnız otuz çift eklerdi. Bu ders bu farkı sezgisel bırakmıyor, sayıyla ölçüyor.

Kaynağın Kendi Uzunluğu

Makronun gerçekte kaç satır ürettiğini çalışma zamanında ölçmenin doğrudan bir yolu yok — genişleme derleme sırasında olur ve çalışan programın eline yalnız sonuç geçer. Ölçülebilir olan, iki yazımın kaynaktaki uzunluğu: makro çağrısının kendisi kaç satır, aynı işi yapan elle yazılmış match bloğu kaç satır. Bu iki kaynak metni birer dize sabiti olarak tutulup satır sayıları karşılaştırılabilir.

Bu, bir önceki bölümdeki koşumdan bağımsız, ayrı bir ölçüm. Önceki bölüm iki yazımın aynı sonucu verdiğini gösterdi; bu bölüm ikisinin aynı işi ne kadar kaynakla yaptığını gösteriyor. İkisi birbirini tamamlıyor: birincisi olmadan ikincisi anlamsız kalırdı — kısa ama yanlış bir yazımı kısa olduğu için övmenin bir anlamı yok. Bu yüzden bu ders satır sayımına geçmeden önce eşitliği koşturarak doğruladı; sıra bilinçli seçildi, tersi olsaydı kısalığın doğruluğun yerine geçtiği izlenimi doğardı.

// satir_sayimi.rs — ayni yonlendirme tanimi iki yazimda tutuluyor, satir sayisi karsilastiriliyor.
fn main() {
    let makro_yazimi = "yonlendirici_olustur!(makro_ile_yonlendir {\n    \"/ana\" => ana_sayfa(),\n    \"/hakkinda\" => hakkinda_sayfa(),\n});";
    let elle_yazim = "fn elle_yonlendir(yol: &str) -> i32 {\n    match yol {\n        \"/ana\" => ana_sayfa(),\n        \"/hakkinda\" => hakkinda_sayfa(),\n        _ => -1,\n    }\n}";

    let makro_satir = makro_yazimi.lines().count();
    let elle_satir = elle_yazim.lines().count();
    println!("makro yazimi: {} satir", makro_satir);
    println!("elle yazim: {} satir", elle_satir);
    println!("kisalan satir: {}", elle_satir - makro_satir);
}
makro yazimi: 4 satir
elle yazim: 7 satir
kisalan satir: 3

MK8. Bu ölçüm, önceki derste --extern yolunun neden ekrana basılmadığını hatırlatan aynı gerekçeyle kuruldu: makronun genişlemesini çalışma zamanında okumanın bir yolu yok, o yüzden ölçüm çalışma zamanına taşınabilecek tek şeyi — kaynağın kendi metnini — ölçüyor. Rust’ın Tasarım Hedefleri dersinde (M10/K01) örtük bir tür dönüşümünün reddi de aynı yöntemle, iki kaynak satırının karakter sayısı karşılaştırılarak ölçülmüştü; burada birim karakter değil satır, ama yöntem aynı: kaynağı çalıştırmak yerine kaynağın kendisini bir veri olarak ele almak.

Elle yazılan yönlendirme yedi satır: işlev imzası, match başlığı, iki dal, yakalayıcı dal, iki kapanış parantezi. Makro çağrısı dört satır: çağrının kendisi, iki yol-işlev çifti, kapanış. Aradaki üç satırlık fark, fn, match ve kapanış parantezlerinin makro tarafında hiç yazılmasına gerek kalmamasından geliyor — bu üç satır, makro tanımının içinde bir kez yazılı ve her çağrıda yeniden üretiliyor. Yol sayısı arttıkça bu oran değişmiyor: her yeni yol-işlev çifti, iki yazımda da birer satır ekliyor, ama iki yazım arasındaki sabit üç satırlık fark hep aynı kalıyor.

Bu sabit maliyet, ölçümün neden “kısalan satır” diye mutlak bir sayı bastığını, “kaç kat kısa” diye bir oran basmadığını da açıklıyor. İki yol için fark üç satır, otuz yol için de fark yine üç satır olurdu — oran küçülür, mutlak fark değişmez. Ölçüm bu yüzden oranı değil farkı basıyor: kursun sayı kuralı gereği bir iddia yalnız gerçekten ölçülen büyüklüğü taşımalı ve burada belirlenimci olarak ölçülebilen şey iki sabit kaynağın satır farkı, bir genelleme değil.

Sınırlayıcı Ölçüm

Okunaklı bir yazım doğruluk getirmiyor. Makro, çağıranın yazdığı yol-işlev çiftlerinin anlamlı olup olmadığını hiç sormuyor — yalnız her çiftin bir dizgi sabiti ve i32 döndüren bir ifade olup olmadığına bakıyor. Bu sınır önceki iki bölümdeki ölçümlerin üzerine ekleniyor, onları geçersiz kılmıyor: makro yazımının aynı sonucu verdiği ve kaynağı kısalttığı doğru kalıyor; ama bu iki kazanç, üçüncü bir soruya — kurulan tablonun doğru olup olmadığına — hiçbir yanıt taşımıyor. Aşağıdaki örnek, bir yolun yanlışlıkla olması gerekenden başka bir işleve bağlandığı bir tabloyu aynı makroyla kuruyor.

// yanlis_ama_dogru_tip.rs — mantiksal olarak yanlis bir esleme de tip denetiminden geciyor.
macro_rules! yonlendirici_olustur {
    ($ad:ident { $($yol:literal => $isleyici:expr),+ $(,)? }) => {
        fn $ad(yol: &str) -> i32 {
            match yol {
                $($yol => $isleyici,)+
                _ => -1,
            }
        }
    };
}

fn basarili() -> i32 { 200 }

yonlendirici_olustur!(hatali_yonlendir {
    "/ana" => basarili(),
    "/hata" => basarili(),
});

fn main() {
    println!("/hata -> {}", hatali_yonlendir("/hata"));
}
/hata -> 200

MK9. /hata yolu, adından da anlaşılacağı gibi bir başarısızlık durumunu temsil etmesi beklenen bir yol; ama tabloyu kuran satır yanlışlıkla basarili() işlevine bağlanmış ve sonuç 200. Makro bunu reddetmiyor, çünkü reddedecek bir bilgisi yok: $isleyici:expr deseni yalnız “burada i32 döndüren bir ifade olacak” diyor, o ifadenin çağrıldığı yolun anlamıyla tutarlı olup olmadığını sormuyor. Derleyici de aynı nedenle sessiz kalıyor — basarili() gerçekten i32 döndürüyor, imza tutuyor, tür denetimi geçiyor. Üretilen gövde yine “sürdürüyor” sınıfına düşüyor, çünkü tür denetimi gerçekten çalışıyor; ama tür denetimi burada “bu doğru mu” sorusuna hiç yanıt vermiyor, yalnız “bu türden mi” sorusuna yanıt veriyor.

Bu örnekte hata elle de aynı kolaylıkla yazılabilirdi — elle_yonlendir içindeki bir dalı yanlış işleve bağlamak da aynı şekilde derlenirdi. Makro burada hatayı kolaylaştırmıyor; yalnız hatayı gizlemiyor da. Fark, iki yazımın da aynı zayıflığı taşıdığı ama makro yazımının bu zayıflığı daha az satırda ve daha az yazımla ulaşılır kıldığı: yanlış bir çift eklemek, otuz satırlık bir match bloğunda bir dalı değiştirmekten daha az dikkat gerektiren bir işlem. Kaynağın kısalması, kaynağın gözden geçirilmesini kolaylaştırıyor ama gözden geçirmenin yerini almıyor.

Bu gözlem, kursun dört sınıfından hiçbirine yeni bir beşinciyi eklemiyor — “yanlış ama tür uyumlu” ayrı bir sınıf değil, “sürdürüyor” sınıfının kendi içindeki bir ayrıntı. Derleyici burada da gerçekten denetliyor, hiçbir adımı atlamıyor; yalnız denetlediği şey, kaynağı okuyan bir insanın sorduğu sorudan daha dar: “bu ifade beklenen türde mi”, “bu bağ doğru mu” değil.

Bu, önceki iki dersteki gözlemlerle aynı sınıfta duran ama farklı bir köşeden gösterilen bir sınır. Bildirimsel Makrolar dersi, aynı makronun i32 ve bool gibi iki anlamca uzak türle çağrılabildiğini, ikisinin de derlenip makronun hangisinin anlamlı olduğunu söyleyemediğini göstermişti. Burada tür sabit (i32), değişen şey bir dizginin hangi ifadeyle bağlandığı — ama sonuç aynı: makro sözdizimi düzeyinde çalışıyor, iki farklı bağın birbirinden anlamca ayrıldığını göremiyor, yalnız ikisinin de biçimce doğru olduğunu görüyor. Okunaklı bir yazım, yalnız yazan tarafın niyetini daha kısa ifade etmesini sağlıyor; o niyetin doğru kurulup kurulmadığını hâlâ okuyan tarafın kontrol etmesi gerekiyor.

Prosedürel Makrolar dersinde görülen ayrım da burada geçerliliğini koruyor: bir prosedürel makro, girdi andacını okuyup üzerinde daha karmaşık bir işlem yapabilseydi bile, yine yalnız tür uygunluğuna bakardı — çünkü Rust’ta tür denetimi her zaman genişlemeden sonra, üretilen kaynak üzerinde çalışıyor, hangi makro biçimi kullanılırsa kullanılsın. İki makro biçiminin (bildirimsel, prosedürel) üretim mekanizması farklı olsa da, ikisinin de vardığı kaynak aynı denetim sürecinden geçiyor ve o denetim sürecinin sınırı da ikisinde aynı: tür doğru mu, anlam doğru mu sorularının yalnız ilkine yanıt veriyor.

Özet

  • Aynı yönlendirme yapısı bir makro yazımıyla ve elle kurulan yazımla oluşturuldu; üç yol için ikisi de birebir aynı sonucu verdi ve bu eşitlik koşturularak, tek tek karşılaştırılarak doğrulandı.
  • Üretilen gövde bu derste de sürdürüyor sınıfına düşüyor: makro çağrısının ürettiği işlev, elle yazılmış işlevle aynı kurallarla denetleniyor.
  • Makro yazımı aynı işi üç satır kısalttı; bu kısalma makronun sabit başlık-kapanış maliyetini bir kez yazıp her çağrıda yeniden kullanmasından geliyor ve yol sayısı arttıkça mutlak farkın kendisi değişmiyor.
  • Okunaklı bir makro yazımı doğruluk garantisi getirmiyor: mantıksal olarak yanlış bir yol-işlev bağı da, türü tuttuğu için aynı kolaylıkla derleniyor.
  • Makronun denetleyebildiği tek şey biçim — bir ifadenin beklenen türden olup olmadığı; o ifadenin çağıranın niyetiyle örtüşüp örtüşmediğini denetleyen bir mekanizma yok.
  • İfade edici bir arayüz kurmak için bildirimsel makro seçildi; prosedürel makro yalnız girdi üzerinde gerçek bir hesaplama gerektiğinde tercih ediliyor, sabit bir kalıbı sarmak için değil.

Sonraki Adım

Bu konunun üç dersi, üretilen kaynağın üç ayrı kurguda — aynı dosyada, ayrı bir derleme adımında, ifade edici bir arayüzün içinde — hep aynı sınıfa, sürdürüyor sınıfına düştüğünü gösterdi. Üçünde de ortak kalan sınır aynıydı: derleyici üretilen kaynağın türünü denetliyor, anlamını denetlemiyor. Sıradaki konu, Düşük Seviye ve Birlikte Çalışma, bu sınırı derinleştiriyor: derleyicinin denetimi hiç durmadığı bu üç dersin ardından, sıradaki ders denetimin gerçekten durduğu bir bloğa giriyor — orada da denetim toptan kapanmıyor, yalnız birkaç işlem “devredildi” sınıfına açılıyor ve geri kalan her şey, tıpkı burada olduğu gibi, sürmeye devam ediyor.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ ↵ Esc gezin · aç · kapat