ch20-06-macros
Makrolar
Bu kitapta println!
gibi makroları kullandık, ancak bir makronun ne olduğunu ve nasıl çalıştığını tam anlamıyla keşfetmedik. Makro terimi, Rust'taki bir dizi özelliği ifade eder: declarative makrolar macro_rules!
ile ve üç tür procedural makrolar:
derive
niteliğiyle birleştirildiğinde eklenen kodu belirten özel#[derive]
makroları- Herhangi bir öğede kullanılabilen özel nitelikler tanımlayan nitelik benzeri makrolar
- Fonksiyon çağrılarına benzeyen, ancak argüman olarak belirtilen tokenlar üzerinde işlem yapan fonksiyon benzeri makrolar
Her birini sırayla ele alacağız, ancak önce, fonksiyonlarımız varken neden makrolara ihtiyaç duyduklarına bir bakalım.
Makrolar ile Fonksiyonlar Arasındaki Fark
Temel olarak, makrolar başka kod yazan bir kod yazma yoludur; bu duruma metaprogramlama denir. Ek B'de, size çeşitli özellikler için bir uygulama oluşturan derive
niteliğinden bahsediyoruz. Ayrıca, bu kitapta println!
ve vec!
makrolarını kullandık. Bu makroların tümü, yazdığınız koddan daha fazla kod üretmek için genleşir.
Metaprogramlama, yazmanız ve bakımını yapmanız gereken kod miktarını azaltmak için yararlıdır; bu aynı zamanda fonksiyonların rollerinden biridir.
Bununla birlikte, makroların fonksiyonların sahip olmadığı bazı ek yetenekleri vardır.
- Bir fonksiyon imzası, fonksiyonun sahip olduğu parametrelerin sayısını ve türünü belirtmelidir.
- Öte yandan, makrolar değişken sayıda parametre alabilir:
println!("merhaba")
çağrısını tek bir argümanla veyaprintln!("merhaba {}", name)
çağrısını iki argümanla yapabiliriz. - Ayrıca, makrolar derleyici kodun anlamını yorumlamadan önce genişletilir, böylece bir makro, örneğin, belirli bir türde bir özelliği uygulayabilir.
Bir fonksiyon, çalışma zamanında çağrılabilir ve bir özelliğin derleme zamanında uygulanması gerekir.
Bir makroyu uygulamanın, bir fonksiyonu uygulamaktan dezavantajı, makro tanımlarının fonksiyon tanımlarından daha karmaşık olmasıdır çünkü Rust kodu yazıyorsunuz ve bu kod başka Rust kodu yazıyor. Bu dolaylılık nedeniyle, makro tanımları genellikle fonksiyon tanımlarından daha zor okunur, anlaşılır ve bakım yapılır.
Makrolar ile fonksiyonlar arasındaki bir diğer önemli fark, bir dosyada bir makroyu çağırmadan önce makroları tanımlamanız veya kapsamınıza getirmeniz gerektiğidir; oysa fonksiyonları istediğiniz yere tanımlayıp çağırabilirsiniz.
Genel Metaprogramlama için macro_rules!
ile Deklaratif Makrolar
Rust'taki en yaygın kullanılan makro biçimi deklaratif makrodur. Bunlar ayrıca “örnekle makrolar”, “macro_rules!
makroları” veya kısaca “makrolar” olarak da adlandırılır. Temelinde, deklaratif makrolar, Rust match
ifadesine benzer bir şey yazmanıza olanak tanır.
Bölüm 6'da ele alındığı gibi, match
ifadeleri, bir ifadeyi alan, ifadenin oluşan değerini kalıplarla karşılaştıran ve ardından eşleşen kalıpla ilişkili kodu çalıştıran kontrol yapılarıdır.
Makrolar ayrıca, belirli bir kodla ilişkilendirilmiş kalıplara bir değeri karşılaştırır: bu durumda değer, makroya geçirilen gerçek Rust kaynak kodudur; kalıplar, bu kaynak kodunun yapısıyla karşılaştırılır; ve eşleşen her kalıpla ilişkili kod, makroya iletilen kodun yerini alır. Tüm bunlar derleme sırasında gerçekleşir.
Bir makro tanımlamak için macro_rules!
yapısını kullanırsınız. macro_rules!
kullanmanın nasıl olduğunu anlamak için vec!
makrosunun nasıl tanımlandığına bakalım. Bölüm 8, vec!
makrosunu belirli değerlerle yeni bir vektör oluşturmak için kullanmamızın yolunu ele alıyordu.
Not:
vec!
makrosunun standart kütüphanedeki gerçek tanımı, önceden doğru miktarda bellek ayırmak için kod içerir. Örnek basıtlaşsın diye bu kodu burada dahil etmiyoruz.
— Docusaurus Markdown Feature Team
let v: Vec<u32> = vec![1, 2, 3];
İki tam sayılık bir vektör ya da beş dize dilimi içeren bir vektör oluşturmak için de vec!
makrosunu kullanabiliriz. Aynı şeyi bir fonksiyonla yapamazdık çünkü önceden değerlerin sayısını veya türünü bilemezdik.
Liste 20-28: vec!
makrosunun basitleştirilmiş bir tanımını gösterir.
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-28/src/lib.rs}}
#[macro_export]
notasyonu, bu makronun, tanımlandığı crate kapsamına getirildiğinde erişilebilir olması gerektiğini belirtir. Bu notasyon olmadan, makro kapsamınıza getirilemez.
Ardından, makro tanımına macro_rules!
ile başlıyoruz ve tanımladığımız makronun ismini ünlem işareti olmadan yazıyoruz.
Bu durumda isim vec
olup, makro tanımının gövdesini belirten küme parantezleriyle takip edilir.
vec!
gövdesindeki yapı, bir match
ifadesinin yapısına benzerdir. Burada, ( $( $x:expr ),* )
kalıbına sahip bir kol var, ardından =>
ve bu kalıpla ilişkili kod bloğu geliyor. Eğer kalıp eşleşirse, ilişkili kod bloğu yayımlanır. Bu makrodaki tek kalıp olduğuna göre, eşleşebilecek sadece bir geçerli yol vardır; başka bir kalıp hata verecektir. Daha karmaşık makroların birden fazla kolu olabilir.
Makro tanımlarındaki geçerli kalıp sözdizimi, Bölüm 19'da ele alınan kalıp sözdiziminden farklıdır çünkü makro kalıpları, değerler yerine Rust kodu yapısına karşı karşılaştırılır. Liste 20-28'deki kalıp parçalarının ne anlama geldiğini inceleyelim; tam makro kalıp sözdizimi için Rust Referansı’na bakın.
Öncelikle, tüm kalıbı kapsamak için bir set parantez kullanıyoruz. Bir makro sisteminde kalıbı karşılayan Rust kodunu içerecek bir değişkeni tanımlamak için bir dolar işareti ($
) kullanıyoruz. Dolar işareti, bunun, normal bir Rust değişkeni yerine bir makro değişkeni olduğunu açıkça belirtir. Ardından, yer değiştirme kodunda kullanılmak üzere parantezler içindeki kalıplara karşılık gelen değerleri yakalayan bir parantez seti gelir. $()
içindeki $x:expr
, herhangi bir Rust ifadesini karşılar ve bu ifadeye $x
adını verir.
$()
'dan sonraki virgül, $()
'daki kodla eşleşen koddan sonra bir harf ayrım karakterinin isteğe bağlı olarak görünebileceğini gösterir. *
simgesi, kalıbın önündeki nesnenin sıfır veya daha fazlasını karşılayabileceğini belirtir.
Bu makroyu vec![1, 2, 3];
ile çağırdığımızda, $x
kalıbı 1
, 2
ve 3
ifadeleriyle üç kez eşleşir.
Şimdi, bu kol ile ilişkili kodun içindeki kalıba bakalım: $()*
içindeki temp_vec.push()
; bu, kalıbın kaç kez eşleştiğine göre, $()
'daki kalıpla eşleşen her parçanın üretilmesi için sıfır veya daha çok kez üretir. $x
, eşleşen her ifadeyle değiştirilir. Bu makroyu vec![1, 2, 3];
ile çağırdığımızda, bu makro çağrısının yerini alacak oluşturulan kod aşağıdaki gibidir:
{
let mut temp_vec = Vec::new();
temp_vec.push(1);
temp_vec.push(2);
temp_vec.push(3);
temp_vec
}
Herhangi bir sayıda argümanı ve her türdeki argümanı alabilen ve belirtilen öğeleri içeren bir vektör oluşturmak için kod üretebilecek bir makro tanımladık.
Makro yazma hakkında daha fazla bilgi edinmek için çevrimiçi belgelere veya Daniel Keep tarafından başlatılan ve Lukas Wirth tarafından devam eden “Rust Makrolarının Küçük Kitabı” gibi diğer kaynaklara başvurun.
Niteliklerden Kod Üretmek için Procedural Makrolar
İkinci makro biçimi procedural makrodur; bu, bir fonksiyona daha çok benzer (ve bir tür prosedürdür). Procedural makrolar, bir kodu girdi olarak alır, o kod üzerinde işlem yapar ve çıktı olarak bazı kodlar üretir; bu, kalıplara karşı eşleşip kodu başka bir kodla değiştiren deklartif makrolarından farklıdır. Üç tür procedural makro vardır: özel türetme, nitelik benzeri ve fonksiyon benzeri; hepsi benzer bir şekilde çalışır.
Procedural makro oluştururken, tanımlarının kendi crate'lerinde ve özel bir crate türü ile yer alması gerekir. Bu, gelecekte ortadan kaldırmayı umduğumuz karmaşık teknik nedenlerden kaynaklanmaktadır. Liste 20-29'da, some_attribute
belirli bir makro türü kullanmak için bir yer tutucudur; bir procedural makro nasıl tanımlanır gösterilmektedir.
Liste 20-29: Bir procedural makro tanımlama örneği
use proc_macro;
#[some_attribute]
pub fn some_name(input: TokenStream) -> TokenStream {
}
Procedural makroyu tanımlayan fonksiyon, bir TokenStream
alır ve bir TokenStream
üretir. TokenStream
türü, Rust ile birlikte gelen ve bir dizi token'ı temsil eden proc_macro
crate'i tarafından tanımlanır. Bu, makronun özü: makronun üzerinde işlem yaptığı kaynak kodu, giriş TokenStream
'ını oluşturur ve makronun ürettiği kod, çıkış TokenStream
'ını oluşturur. Fonksiyona, hangi tür procedural makro oluşturduğumuzu belirten bir nitelik de eklenmiştir. Aynı crate'de birden fazla türde procedural makroya sahip olabiliriz.
Şimdi, farklı türdeki procedural makrolara bakalım. Özel türetme makrosuyla başlayalım ve ardından diğer türlerin farklı olmasını sağlayan küçük farklılıkları açıklayalım.
Özel derive
Makrosu Nasıl Yazılır
hello_macro
adında bir crate oluşturalım; bu crate bir HelloMacro
trait'i tanımlar ve bununla ilişkili bir hello_macro
fonksiyonu içerir. Kullanıcılarımızın her bir türü için HelloMacro
trait'ini uygulamalarını istemek yerine, bir prosedürel makro sağlayacağız. Böylece kullanıcılar türlerini #[derive(HelloMacro)]
ile not edebilir ve hello_macro
fonksiyonunun varsayılan uygulamasını alabilirler. Varsayılan uygulama, Hello, Macro! Benim adım TypeName!
yazdıracaktır; burada TypeName
, bu trait'in tanımlandığı türün adıdır. Diğer bir deyişle, başka bir programcının bu crate'i kullanarak 20-30. Listede olduğu gibi kod yazmasını sağlayan bir crate yazacağız.
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-30/src/main.rs}}
Bu kod tamamlandığında Hello, Macro! Benim adım Pancakes!
yazacaktır. İlk adım, yeni bir kütüphane crate'i oluşturmaktır, şöyle:
$ cargo new hello_macro --lib
Sonra, HelloMacro
trait'ini ve onunla ilişkili fonksiyonu tanımlayacağız:
{{#rustdoc_include ../listings/ch20-advanced-features/no-listing-20-impl-hellomacro-for-pancakes/hello_macro/src/lib.rs}}
Bir trait ve onun fonksiyonuna sahibiz. Bu noktada, crate kullanıcımız gerekli işlevselliği elde etmek için trait'i uygulayabilir:
{{#rustdoc_include ../listings/ch20-advanced-features/no-listing-20-impl-hellomacro-for-pancakes/pancakes/src/main.rs}}
Ancak, hello_macro
ile kullanmak istedikleri her tür için uygulama bloğu yazmak zorunda kalacaklardı; bu çalışmayı onlardan esirgeyebilmek istiyoruz.
Ayrıca, trait'in uygulandığı türün adını yazdıracak varsayılan bir hello_macro
fonksiyonu sağlayamayız: Rust, çalışma zamanında tür adını aramak için yansıma yeteneklerine sahip değildir. Derleme zamanında kod üretecek bir makroya ihtiyacımız var.
Bir sonraki adım, prosedürel makroyu tanımlamaktır. Bu yazının yazıldığı zamanlarda, prosedürel makroların kendi crate'lerinde bulunması gerekmektedir. Sonunda, bu kısıtlama kaldırılabilir. Crate'leri ve makro crate'lerini yapılandırma konvansiyonu şu şekildedir: foo
adında bir crate için, özel bir derive prosedürel makro crate'i foo_derive
olarak adlandırılır. hello_macro
projemiz içinde hello_macro_derive
adında yeni bir crate başlatalım:
$ cargo new hello_macro_derive --lib
İki crate'imiz sıkı bir şekilde ilişkilidir, bu yüzden prosedürel makro crate'ini hello_macro
crate'imizin dizininde oluşturuyoruz.
Eğer hello_macro
'daki trait tanımını değiştirirsek, hello_macro_derive
'deki prosedürel makronun uygulamasını da değiştirmemiz gerekecek. İki crate ayrı olarak yayımlanmalıdır ve bu crate'leri kullanan programcılar ayrıca her ikisini de bağımlılık olarak eklemeli ve her ikisini de kapsam içine almalıdır. Alternatif olarak, hello_macro
crate'i hello_macro_derive
'yi bir bağımlılık olarak kullanıp prosedürel makro kodunu yeniden dışa aktarabilir. Ancak projemizi yapılandırma şeklimiz, programcıların derive
işlevselliğini istemeseler bile hello_macro
'yu kullanmalarına olanak tanır.
hello_macro_derive
crate'ini bir prosedürel makro crate olarak ilan etmemiz gerekiyor.
Ayrıca, syn
ve quote
crate'lerinden de işlevsellik alacağız, bunu birazdan göreceksiniz; bu yüzden bunları bağımlılıklar olarak eklememiz gerekiyor. Aşağıdakileri hello_macro_derive
için Cargo.toml dosyasına ekleyin:
{{#include ../listings/ch20-advanced-features/listing-20-31/hello_macro/hello_macro_derive/Cargo.toml:6:12}}
Prosedürel makroyu tanımlamaya başlamak için, Listing 20-31'deki kodu hello_macro_derive
crate'inin src/lib.rs dosyasına yerleştirin. Bu kod, impl_hello_macro
fonksiyonu için bir tanım ekleyene kadar derlenmeyecektir.
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-31/hello_macro/hello_macro_derive/src/lib.rs}}
TokenStream
'i parse etmekten sorumlu olan hello_macro_derive
fonksiyonu ve sözdizimi ağacını dönüştürmekten sorumlu olan impl_hello_macro
fonksiyonu olarak kodu böldüğümüzü belirtmek isteriz: bu, bir prosedürel makro yazmayı daha rahat hale getirir.
Dıştaki fonksiyondaki (hello_macro_derive
bu durumda) kod, göreceğiniz ya da oluşturacağınız hemen hemen her prosedürel makro crate'i için aynı olacaktır. İçteki fonksiyonun gövdesinde (impl_hello_macro
bu durumda) tanımladığınız kod, prosedürel makronuzun amacına bağlı olarak farklı olacaktır.
Üç yeni crate tanıttık: proc_macro
, syn
ve quote
. proc_macro
, Rust ile birlikte gelir, bu yüzden bunu Cargo.toml'da bağımlılıklara eklemek zorunda kalmadık. proc_macro
, Rust kodunu kodumuzdan okuma ve üzerinde değişiklik yapma yeteneği sağlayan derleyicinin API'sidir.
syn
crate'i Rust kodunu bir string'den, üzerinde işlem yapabileceğimiz bir veri yapısına parse eder. quote
crate'i syn
veri yapılarını tekrar Rust koduna çevirir. Bu crate'ler, herhangi bir Rust kodunu parse etmeyi çok daha kolay hale getirir: Rust kodu için tamamen bir parser yazmak basit bir iş değildir.
hello_macro_derive
fonksiyonu, kütüphanemizin bir kullanıcısı tür üzerinde #[derive(HelloMacro)]
belirttiğinde çağrılır. Bu, burada hello_macro_derive
fonksiyonumuzu proc_macro_derive
ile anotasyonlayıp, trait adımızla eşleşen HelloMacro
adını belirttiğimiz için mümkündür; bu, çoğu prosedürel makronun takip ettiği konvansiyondur.
:::quote
Önemli Not: hello_macro_derive
fonksiyonu, input
'u bir TokenStream
'den, ardından işleyeceğimiz ve üzerinde işlem yapabileceğimiz bir veri yapısına dönüştürür.
— Rust Dokümantasyonu
:::
İşte burada syn
devreye giriyor. syn
içindeki parse
fonksiyonu, bir TokenStream
alır ve parse edilmiş Rust kodunu temsil eden bir DeriveInput
struct'ı döndürür. Listing 20-32, struct Pancakes;
stringini parse ettiğimizde elde ettiğimiz DeriveInput
struct'ının ilgili kısımlarını gösterir:
DeriveInput {
// --snip--
ident: Ident {
ident: "Pancakes",
span: #0 bytes(95..103)
},
data: Struct(
DataStruct {
struct_token: Struct,
fields: Unit,
semi_token: Some(
Semi
)
}
)
}
Bu struct'ın alanları, parse ettiğimiz Rust kodunun bir birim struct olduğunu ve ident
'in (kimlik, yani isim) Pancakes olduğunu gösterir. Bu struct'ta, Rust kodunu tanımlamak için daha fazla alan bulunmaktadır; daha fazla bilgi için syn
belgelerindeki DeriveInput
bölümüne bakabilirsiniz.
Yakında impl_hello_macro
fonksiyonunu tanımlayacağız; burada eklemek istediğimiz yeni Rust kodunu inşa edeceğiz. Ama önce, derive makromuzun çıktısının da bir TokenStream
olduğunu belirtmeliyim. Döndürülen TokenStream
, crate kullanıcılarımızın yazdığı koda eklenir. Dolayısıyla, crate'lerini derlediklerinde sağladığımız ek işlevselliği elde ederler.
unwrap
çağrısı yaparak hello_macro_derive
fonksiyonunun syn::parse
fonksiyonuna yapılan çağrı başarısız olursa panik yaratacağını fark etmiş olabilirsiniz. Prosedürel makromuzun hatalar sırasında panik yaratması gereklidir çünkü proc_macro_derive
fonksiyonları TokenStream
döndürmeli, Result
değil, prosedürel makro API'sine uymak için.
Bu örneği unwrap
kullanarak basitleştirdik; üretim kodunda, panic!
ya da expect
kullanarak nelerin yanlış gittiği hakkında daha spesifik hata mesajları sağlamalısınız.
Artık, TokenStream
'den anotasyonlu Rust kodunu bir DeriveInput
örneğine dönüştürecek kodumuz olduğuna göre, Listing 20-33'te gösterildiği gibi, anotasyonlu tür üzerinde HelloMacro
trait'ini uygulamak için kodu üretelim.
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-33/hello_macro/hello_macro_derive/src/lib.rs:here}}
ast.ident
kullanarak annotasyona sahip türün adını (kimliğini) içeren bir Ident
struct örneği alıyoruz. Listing 20-32'deki struct, Listing 20-30
'daki kodu üzerinde impl_hello_macro
fonksiyonunu çalıştırdığımızda, aldığımız ident
'in ident
alanının değeri "Pancakes"
olacaktır.
Böylece, Listing 20-33'teki name
değişkeni, yazdırıldığında, Listing 20-30'daki struct'ın adı olan "Pancakes"
stringini elde edeceğimiz bir Ident
struct örneğini içerecektir.
quote!
makrosu, döndürmek istediğimiz Rust kodunu tanımlamamız için bize olanak tanır. Derleyici, quote!
makrosunun doğrudan sonucunun beklenen şeye benzemesini istediği için, onu bir TokenStream
'e dönüştürmemiz gerekmektedir. Bunu yapmak için into
metodunu çağırıyoruz; bu, bu ara temsili tüketir ve gereken TokenStream
türünde bir değeri döndürür.
quote!
makrosu ayrıca bazı çok havalı şablonlama mekanikleri sağlar: #name
yazabiliriz; quote
bunu name
değişkenindeki değerle değiştirecektir. Düzenli makroların çalışma şekliyle benzer bir şekilde tekrar yapabilirsiniz.
quote crate'inin belgelerini inceleyerek kapsamlı bir giriş yapabilirsiniz.
Prosedürel makromuzun kullanıcının annotasyonu olan tür için HelloMacro
trait'inin bir uygulamasını üretmesini istiyoruz; bunu #name
kullanarak elde edeceğiz. Trait uygulaması, Hello, Macro! Benim adım
yazdırır ve ardından anotasyonlu türün adı gelir.
Burada kullanılan stringify!
makrosu, Rust'a gömülü bir makrodur. Bir Rust ifadesini, örneğin 1 + 2
, alır ve derleme zamanında ifadeyi string literali olan "1 + 2"
haline dönüştürür. Bu, ifadenin değerini değerlendiren ve ardından sonucu bir String
haline getiren format!
veya println!
gibi makrolardan farklıdır. Girişteki #name
ifadesinin harfiyen yazılması gereken bir ifade olma olasılığı bulunmaktadır, bu yüzden stringify!
kullanıyoruz. stringify!
kullanmak ayrıca #name
'i derleme zamanında bir string literaline çevirerek bir tahsisat kazandırır.
Artık cargo build
hem hello_macro
hem de hello_macro_derive
kısmında başarılı bir şekilde tamamlanmalıdır. Prosedürel makro eylemini görmek için bu crate'leri Listing 20-30'da bulunan kodla bağlayalım! projects dizininizde cargo new pancakes
kullanarak yeni bir ikili proje oluşturun. pancakes
crate'inin Cargo.toml dosyasına hello_macro
ve hello_macro_derive
'yi bağımlılık olarak eklememiz gerekiyor. Eğer hello_macro
ve hello_macro_derive
'nin sürümlerini crates.io üzerinde yayınlıyorsanız, bunlar normal bağımlılıklar olacaktır; eğer değilse, bunları path
bağımlılıkları olarak aşağıdaki gibi belirtebilirsiniz:
{{#include ../listings/ch20-advanced-features/no-listing-21-pancakes/pancakes/Cargo.toml:7:9}}
Listing 20-30'un kodunu src/main.rs dosyasına yerleştirin ve cargo run
çalıştırın: Hello, Macro! Benim adım Pancakes!
yazdırmalıdır. Prosedürel makroya ait HelloMacro
trait'inin uygulaması eklenmiştir; pancakes
crate'inin bunu uygulamasına gerek kalmamıştır; #[derive(HelloMacro)]
trait uygulamasını eklemiştir.
Sonraki adım, diğer prosedürel makro türlerinin özel derive makrolarından nasıl farklılaştığını keşfetmektir.
Attribute-benzeri makrolar
Attribute-benzeri makrolar, özel derive makrolarına benzer, ancak derive
niteliği için kod ürettikleri yerine, yeni nitelikler oluşturmanıza olanak tanır. Ayrıca daha esnektirler: derive
yalnızca struct'lar ve enum'lar için çalışırken; nitelikler diğer öğelere de uygulanabilir, örneğin fonksiyonlara. Attribute-benzeri bir makro kullanma örneği: diyelim ki, bir web uygulama çerçevesi kullanırken fonksiyonları etiketleyen route
adlı bir nitelik var:
#[route(GET, "/")]
fn index() {
Bu #[route]
niteliği, çerçeve tarafından bir prosedürel makro olarak tanımlanır. Makro tanım fonksiyonunun imzası şu şekilde görünür:
#[proc_macro_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {
Burada, TokenStream
türünde iki parametreye sahibiz. İlk parametre niteliğin içeriğidir: GET, "/"
kısmı. İkinci parametre, niteliğin iliştirildiği öğenin gövdesidir: bu durumda, fn index() {}
ve fonksiyonun geri kalanı.
Bundan başka, attribute-benzeri makrolar, özel derive makrolarıyla aynı şekilde çalışır: proc-macro
crate türü ile bir crate oluşturursunuz ve istediğiniz kodu üreten bir fonksiyon implement edersiniz!
Fonksiyon-benzeri makrolar
Fonksiyon-benzeri makrolar, fonksiyon çağrıları gibi görünen makroları tanımlar. macro_rules!
makroları gibi, bunlar fonksiyonlardan daha esnektirler; örneğin, bilinmeyen sayıda argüman alabilirler. Ancak, macro_rules!
makroları yalnızca daha önceki bölümde tartıştığımız gibi eşleşme benzeri sözdizim kullanılarak tanımlanabilir. Fonksiyon-benzeri makrolar bir TokenStream
parametresi alır ve tanımları, diğer iki prosedürel makro türü gibi, bu TokenStream
üzerinde Rust kodu kullanarak manipülasyon yapar. Fonksiyon-benzeri bir makro örneği, şöyle çağrılabilecek bir sql!
makrosudur:
let sql = sql!(SELECT * FROM posts WHERE id=1);
Bu makro, içindeki SQL ifadesini parse eder ve sözdizimsel olarak doğru olup olmadığını kontrol eder; bu, macro_rules!
makrosunun yapabileceğinden çok daha karmaşık bir işlemedir. sql!
makrosu, şu şekilde tanımlanır:
#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {
Bu tanım, özel derive makrosunun imzasına benzerdir: parantez içindeki token'ları alır ve üretmek istediğimiz kodu döndürür.
Özet
Uff! Artık kutunuzda sıkça kullanmayacağınız bazı Rust özellikleri var, ancak bu özelliklerin özel durumlarda mevcut olduğunu bileceksiniz. Hata mesajı önerileri veya diğer insanların kodlarında karşılaştığınızda, bu kavramları ve sözdizimini tanıyabilmeniz için bir dizi karmaşık konuyu tanıttık. Bu bölümü çözümler bulmanıza rehberlik etmek için bir referans olarak kullanın.
Sonraki adımda, kitap boyunca tartıştığımız her şeyi pratiğe dökeceğiz ve bir proje daha gerçekleştireceğiz!