Kod Yazma Kod: Modern Metaprogramlama Teorisi ve Pratiğine Giriş
Yayınlanan: 2022-07-22Ne zaman makroları açıklamanın en iyi yolunu düşünsem, programlamaya ilk başladığımda yazdığım bir Python programı aklıma gelir. İstediğim gibi organize edemedim. Bir dizi biraz farklı işlevi çağırmak zorunda kaldım ve kod hantal hale geldi. Aradığım şey - o zamanlar bilmememe rağmen - metaprogramlamaydı .
metaprogramming (isim)
Bir programın kodu veri olarak ele alabileceği herhangi bir teknik.
Evcil hayvan sahipleri için bir uygulamanın arka ucunu oluşturduğumuzu hayal ederek Python projemde karşılaştığım sorunları gösteren bir örnek oluşturabiliriz. pet_sdk
kütüphanesindeki araçları kullanarak, evcil hayvan sahiplerinin kedi maması satın almasına yardımcı olmak için Python yazıyoruz:
Kodun çalıştığını doğruladıktan sonra aynı mantığı iki tür daha evcil hayvan (kuşlar ve köpekler) için uygulamaya geçiyoruz. Ayrıca veteriner randevuları için bir özellik de ekliyoruz:
Snippet 2'nin tekrarlayan mantığını bir döngüde yoğunlaştırmak iyi olurdu, bu yüzden kodu yeniden yazmaya koyulduk. Her işlev farklı şekilde adlandırıldığından, döngümüzde hangisini (örneğin, book_bird_appointment
, book_cat_appointment
) çağıracağımızı belirleyemeyeceğimizi hemen fark ederiz:
İstediğimiz son kodu otomatik olarak oluşturan programlar yazabildiğimiz, programımızı bir liste, bir dosyadaki veri veya herhangi bir şeymiş gibi esnek, kolay ve akıcı bir şekilde değiştirebileceğimiz, turboşarjlı bir Python sürümünü hayal edelim. diğer ortak veri türü veya program girişi:
Bu, Rust, Julia veya C gibi dillerde kullanılabilen bir makro örneğidir, ancak Python değil.
Bu senaryo, kendi kodunu değiştirebilen ve değiştirebilen bir program yazmanın nasıl faydalı olabileceğinin harika bir örneğidir. Bu tam olarak makroların çizimidir ve daha büyük bir sorunun birçok yanıtından biridir: Bir programın kendi kodunu iç incelemesini, onu veri olarak işlemesini ve ardından bu iç gözleme göre hareket etmesini nasıl sağlayabiliriz?
Genel olarak, bu tür bir iç gözlemi gerçekleştirebilen tüm teknikler, "metaprogramming" genel terimi altına girer. Metaprogramlama, programlama dili tasarımında zengin bir alt alandır ve önemli bir konsepte kadar izlenebilir: veri olarak kod.
Yansıma: Python Savunmasında
Python makro desteği sağlamasa da, bu kodu yazmak için birçok başka yol sunduğunu belirtebilirsiniz. Örneğin, burada animal
değişkenimizin bir örneği olduğu sınıfı tanımlamak için isinstance()
yöntemini kullanıyoruz ve uygun işlevi çağırıyoruz:
Bu tür metaprogramlama yansıması diyoruz ve buna daha sonra geri döneceğiz. Snippet 5'in kodu hala biraz hantal ama bir programcı için yazması, listelenen her hayvan için mantığı tekrarladığımız Snippet 2'lerden daha kolay.
Meydan okumak
getattr
yöntemini kullanarak, uygun order_*_food
ve book_*_appointment
işlevlerini dinamik olarak çağırmak için önceki kodu değiştirin. Bu muhtemelen kodu daha az okunabilir hale getirir, ancak Python'u iyi biliyorsanız, isinstance
işlevi yerine getattr
nasıl kullanabileceğinizi düşünmeye ve kodu basitleştirmeye değer.
Homooniklik: Lisp'in Önemi
Lisp gibi bazı programlama dilleri, metaprogramlama kavramını homoiconicity aracılığıyla başka bir düzeye taşır.
homoikoniklik (isim)
Bir programlama dilinin özelliği, bu sayede kod ile bir programın üzerinde çalıştığı veriler arasında hiçbir ayrım yoktur.
1958'de oluşturulan Lisp, en eski eşsesli dil ve ikinci en eski üst düzey programlama dilidir. Adını “LISt İşlemci”den alan Lisp, bilgisayarların nasıl kullanıldığını ve programlandığını derinden şekillendiren bilgi işlemde bir devrimdi. Lisp'in programlamayı ne kadar temel ve ayırt edici bir şekilde etkilediğini abartmak zor.
Emacs, güzel olan tek bilgisayar dili olan Lisp ile yazılmıştır. Neal Stephenson
Lisp, FORTRAN'dan sadece bir yıl sonra, delikli kartlar ve bir odayı dolduran askeri bilgisayarlar çağında yaratıldı. Yine de programcılar bugün hala yeni, modern uygulamalar yazmak için Lisp'i kullanıyor. Lisp'in birincil yaratıcısı John McCarthy, AI alanında bir öncüydü. Uzun yıllar boyunca, Lisp, araştırmacıların kendi kodlarını dinamik olarak yeniden yazma becerisine değer verdiği yapay zekanın diliydi. Günümüzün AI araştırması, bu tür mantık oluşturma kodundan ziyade sinir ağları ve karmaşık istatistiksel modeller etrafında toplanmıştır. Bununla birlikte, AI üzerinde Lisp kullanılarak yapılan araştırma - özellikle 60'lı ve 70'li yıllarda MIT ve Stanford'da yapılan araştırma - bildiğimiz alanı yarattı ve muazzam etkisi devam ediyor.
Lisp'in ortaya çıkışı, ilk programcıları özyineleme, üst düzey işlevler ve bağlantılı listeler gibi şeylerin pratik hesaplama olasılıklarına ilk kez maruz bıraktı. Ayrıca lambda hesabı fikirleri üzerine kurulmuş bir programlama dilinin gücünü de gösterdi.
Bu kavramlar, programlama dillerinin tasarımında bir patlamaya yol açtı ve bilgisayar biliminin en büyük isimlerinden biri olan Edsger Dijkstra'nın dediği gibi, “ ...
Bu örnek, girdisinin faktöriyelini yinelemeli olarak hesaplayan ve bu işlevi “7” girişiyle çağıran bir “faktöriyel” fonksiyonunu tanımlayan basit bir Lisp programını (ve daha bilinen Python sözdizimindeki eşdeğerini) göstermektedir:
Lisp | piton |
---|---|
( defun factorial ( n ) ( if ( = n 1 ) 1 ( * n ( factorial ( - n 1 ))))) ( print ( factorial 7 )) | |
Veri Olarak Kodla
Lisp'in en etkili ve sonuç getiren yeniliklerinden biri olmasına rağmen, özyineleme ve Lisp'in öncülük ettiği diğer birçok kavramın aksine eşseslilik, günümüzün programlama dillerinin çoğuna girmedi.
Aşağıdaki tablo, hem Julia hem de Lisp'te kod döndüren eşsesli işlevleri karşılaştırır. Julia, birçok yönden aşina olabileceğiniz üst düzey dillere (örneğin Python, Ruby) benzeyen eşsesli bir dildir.
Her örnekteki sözdiziminin anahtar parçası, alıntı karakteridir. Julia alıntı yapmak için bir :
(iki nokta üst üste) kullanırken, Lisp bir '
(tek tırnak işareti) kullanır:
Julia | Lisp |
---|---|
function function_that_returns_code() return :(x + 1 ) end | |
Her iki örnekte de, ana ifadenin ( (x + 1)
veya (+ x 1)
) yanındaki alıntı, onu doğrudan değerlendirilebilecek koddan değiştirebileceğimiz soyut bir ifadeye dönüştürür. İşlev, bir dize veya veri değil, kod döndürür. Eğer fonksiyonumuzu çağıracak ve print(function_that_returns_code())
yazacak olsaydık, Julia x+1
olarak dizilmiş kodu yazdırırdı (ve eşdeğeri Lisp için geçerlidir). Tersine, :
(veya '
Lisp'te) olmadan, x
tanımlanmadığına dair bir hata alırdık.
Julia örneğimize dönelim ve genişletelim:
eval
işlevi, programın başka bir yerinden oluşturduğumuz kodu çalıştırmak için kullanılabilir. Yazdırılan değerin x
değişkeninin tanımına dayandığını unutmayın. eval
kodu x
tanımlı olmadığı bir bağlamda değerlendirmeye çalışırsak hata alırız.
Homoiconicity, programların anında uyum sağlayabileceği yeni ve karmaşık programlama paradigmalarının kilidini açabilen, etki alanına özgü sorunlara veya karşılaşılan yeni veri biçimlerine uyacak kod üreten güçlü bir metaprogramlama türüdür.
Homoiconic Wolfram Dilinin inanılmaz bir dizi soruna uyum sağlamak için kod üretebildiği WolframAlpha örneğini alın. WolframAlpha'ya, "New York şehrinin GSYİH'sinin Andorra nüfusuna bölünmesi ne kadar?" diye sorabilirsiniz. ve dikkat çekici bir şekilde, mantıklı bir yanıt alır.
Herhangi birinin bu belirsiz ve anlamsız hesaplamayı bir veritabanına dahil etmeyi düşünmesi pek olası görünmüyor, ancak Wolfram bu soruyu yanıtlamak için anında kod yazmak için metaprogramlama ve ontolojik bir bilgi grafiği kullanıyor.
Lisp ve diğer eşsesli dillerin sağladığı esnekliği ve gücü anlamak önemlidir. Daha fazla dalmadan önce, emrinizde olan bazı metaprogramlama seçeneklerini ele alalım:
Tanım | Örnekler | Notlar | |
---|---|---|---|
homoikoniklik | Kodun "birinci sınıf" veri olduğu bir dil özelliği. Kod ve veri arasında ayrım olmadığından, ikisi birbirinin yerine kullanılabilir. |
| Burada Lisp, Lisp ailesindeki Scheme, Racket ve Clojure gibi diğer dilleri içerir. |
makrolar | Girdi olarak kodu alan ve çıktı olarak kodu döndüren bir ifade, işlev veya ifade. |
| (C'nin makroları hakkında sonraki nota bakın.) |
Önişlemci Yönergeleri (veya Ön Derleyici) | Bir programı girdi olarak alan ve kodda yer alan ifadelere dayanarak programın değiştirilmiş bir sürümünü çıktı olarak döndüren bir sistem. |
| C'nin makroları, C'nin önişlemci sistemi kullanılarak uygulanır, ancak ikisi ayrı kavramlardır.#define önişlemci yönergesini kullandığımız C makroları ile diğer C önişlemci yönergeleri (örneğin #if ve #ifndef ) arasındaki temel kavramsal fark, makroları kod oluşturmak için #define olmayan diğer yönergeleri kullanırken kullanmamızdır. diğer kodu koşullu olarak derlemek için önişlemci yönergeleri. İkisi C'de ve diğer bazı dillerde yakından ilişkilidir, ancak bunlar farklı metaprogramlama türleridir. |
Refleks | Bir programın kendi kodunu inceleme, değiştirme ve iç gözlem yapma yeteneği. |
| Yansıma, derleme zamanında veya çalışma zamanında meydana gelebilir. |
jenerik | Bir dizi farklı tür için geçerli olan veya birden çok bağlamda kullanılabilen ancak tek bir yerde saklanabilen kod yazma yeteneği. Kodun açık veya örtük olarak geçerli olduğu bağlamları tanımlayabiliriz. | Şablon tarzı jenerikler:
Parametrik polimorfizm:
| Genel programlama, genel metaprogramlamaya göre daha geniş bir konudur ve ikisi arasındaki çizgi iyi tanımlanmamıştır. Bu yazarın görüşüne göre, bir parametrik tür sistemi, yalnızca statik olarak yazılmış bir dildeyse metaprogramlama olarak sayılır. |
Çeşitli programlama dillerinde yazılmış bazı eşseslilik, makrolar, önişlemci yönergeleri, yansıma ve jeneriklerin uygulamalı örneklerine bakalım:
Makrolar (Snippet 11'deki gibi) yeni nesil programlama dillerinde yeniden popüler hale geliyor. Bunları başarılı bir şekilde geliştirmek için kilit bir konuyu dikkate almalıyız: hijyen.
Hijyenik ve Hijyenik Olmayan Makrolar
Kodun “hijyenik” veya “hijyenik olmayan” olması ne anlama gelir? Açıklığa kavuşturmak için, macro_rules!
işlev. Adından da anlaşılacağı gibi, macro_rules!
tanımladığımız kurallara göre kod üretir. Bu durumda, makromuzu my_macro
olarak adlandırdık ve kural “ let x = $n
kod satırını oluşturun” şeklindedir, burada n
bizim girdimizdir:
Makromuzu genişlettiğimizde (çağrımını oluşturduğu kodla değiştirmek için bir makro çalıştırarak), aşağıdakileri almayı bekleriz:
Görünüşe göre, makromuz x
değişkenini 3'e eşit olarak yeniden tanımladı, bu nedenle programın 3
yazdırmasını makul bir şekilde bekleyebiliriz. Aslında, 5
yazdırır! Şaşırmış? Rust'ta macro_rules!
tanımlayıcılara göre hijyeniktir, bu nedenle kapsamı dışındaki tanımlayıcıları "yakalamaz". Bu durumda, tanımlayıcı x
idi. Makro tarafından yakalanmış olsaydı, 3'e eşit olurdu.
hijyen (isim)
Bir makronun genişlemesinin, makronun kapsamı dışından tanımlayıcıları veya diğer durumları yakalamayacağını garanti eden bir özellik. Bu özelliği sağlamayan makro ve makro sistemlere hijyenik olmayan denir.
Makrolarda hijyen, geliştiriciler arasında biraz tartışmalı bir konudur. Savunucuları, hijyen olmadan kodunuzun davranışını kazara kurnazca değiştirmenin çok kolay olduğu konusunda ısrar ediyor. Birçok değişken ve diğer tanımlayıcılarla karmaşık kodda kullanılan Snippet 13'ten önemli ölçüde daha karmaşık bir makro düşünün. Ya o makro, kodunuzla aynı değişkenlerden birini kullandıysa ve siz fark etmediyseniz?
Bir geliştiricinin, kaynak kodunu okumadan harici bir kitaplıktan makro kullanması alışılmadık bir durum değildir. Bu, özellikle makro desteği sunan yeni dillerde yaygındır (örneğin, Rust ve Julia):
C'deki bu hijyenik olmayan makro, tanımlayıcı website
yakalar ve değerini değiştirir. Elbette, tanımlayıcı yakalama kötü amaçlı değildir. Bu sadece makro kullanmanın tesadüfi bir sonucudur.
Yani hijyenik makrolar iyidir ve hijyenik olmayan makrolar kötüdür, değil mi? Ne yazık ki, o kadar basit değil. Hijyenik makroların bizi sınırladığına dair güçlü bir kanıt var. Bazen tanımlayıcı yakalama yararlıdır. Üç tür evcil hayvan için hizmet sağlamak için pet_sdk
kullandığımız Snippet 2'ye tekrar bakalım. Orijinal kodumuz şöyle başladı:
Snippet 3'ün, Snippet 2'nin tekrarlayan mantığını her şey dahil bir döngüde yoğunlaştırma girişimi olduğunu hatırlayacaksınız. Ama ya kodumuz cats
and dogs
tanımlayıcılarına bağlıysa ve aşağıdaki gibi bir şey yazmak istiyorsak:
Snippet 16 elbette biraz basittir, ancak bir makronun belirli bir kod bölümünün %100'ünü yazmasını istediğimiz bir durum hayal edin. Hijyenik makrolar böyle bir durumda sınırlayıcı olabilir.
Hijyenik ve hijyenik olmayan makro tartışması karmaşık olsa da, iyi haber şu ki, bu sizin bir duruş sergilemeniz gereken bir konu değil. Kullandığınız dil, makrolarınızın hijyenik olup olmayacağını belirler, bu nedenle makroları kullanırken bunu aklınızda bulundurun.
Modern Makrolar
Makrolar şimdi biraz zaman geçiriyor. Uzun bir süre boyunca, modern zorunlu programlama dillerinin odak noktası, işlevlerinin temel bir parçası olarak makrolardan uzaklaştı ve onları diğer metaprogramlama türleri lehine çevirdi.
Okullarda yeni programcılara öğretilen diller (örneğin Python ve Java) onlara tek ihtiyaçları olan şeyin yansıma ve jenerik olduğunu söyledi.
Zamanla, bu modern diller popüler hale geldikçe, makrolar göz korkutucu C ve C++ önişlemci sözdizimi ile ilişkilendirildi - programcılar bunların farkında bile olsalar.
Ancak Rust ve Julia'nın ortaya çıkmasıyla birlikte trend makrolara geri döndü. Rust ve Julia, makro kavramını bazı yeni ve yenilikçi fikirlerle yeniden tanımlayan ve popülerleştiren iki modern, erişilebilir ve yaygın olarak kullanılan dillerdir. Bu, kullanımı kolay, "piller dahil" çok yönlü bir dil olarak Python ve R'nin yerini almaya hazır görünen Julia'da özellikle heyecan verici.
“TurboPython” gözlüklerimizle pet_sdk
ilk baktığımızda gerçekten istediğimiz Julia gibi bir şeydi. Eş sesliliğini ve sunduğu diğer metaprogramlama araçlarından bazılarını kullanarak Snippet 2'yi Julia'da yeniden yazalım:
Snippet 17'yi parçalayalım:
- Üç tuple üzerinden yineliyoruz. Bunlardan ilki
("cat", :clean_litterbox)
'dur, bu nedenlepet
değişkeni"cat"
öğesine,care_fn
değişkeni ise alıntılanan:clean_litterbox
sembolüne atanır. - Bir dizgiyi bir
Expression
dönüştürmek içinMeta.parse
işlevini kullanırız, böylece onu kod olarak değerlendirebiliriz. Bu durumda, hangi işlevin çağrılacağını tanımlamak için bir dizgiyi diğerine koyabileceğimiz dizge enterpolasyonunun gücünü kullanmak istiyoruz. - Oluşturduğumuz kodu çalıştırmak için
eval
işlevini kullanırız.@eval begin… end
, kodu yeniden yazmaktan kaçınmak içineval(...)
yazmanın başka bir yoludur.@eval
bloğunun içinde dinamik olarak oluşturduğumuz ve çalıştığımız kod var.
Julia'nın metaprogramlama sistemi, istediğimiz şeyi istediğimiz şekilde ifade etmemiz için bizi gerçekten özgürleştiriyor. Yansıma da dahil olmak üzere başka birkaç yaklaşım kullanabilirdik (Snippet 5'teki Python gibi). Ayrıca, belirli bir hayvan için açıkça kod üreten bir makro işlevi de yazabilirdik veya tüm kodu bir dize olarak oluşturup Meta.parse
veya bu yöntemlerin herhangi bir kombinasyonunu kullanabilirdik.
Julia'nın Ötesinde: Diğer Modern Metaprogramlama Sistemleri
Julia, modern bir makro sistemin belki de en ilginç ve en zorlayıcı örneklerinden biridir, ancak hiçbir şekilde tek örnek değildir. Rust da makroları bir kez daha programcıların önüne getirmede etkili oldu.
Rust'ta, makrolar Julia'dakinden çok daha merkezidir, ancak bunu burada tam olarak keşfetmeyeceğiz. Pek çok nedenden dolayı, makro kullanmadan deyimsel Rust yazamazsınız. Julia'da ise, eşseslilik ve makro sistemi tamamen görmezden gelmeyi seçebilirsiniz.
Bu merkeziliğin doğrudan bir sonucu olarak, Rust ekosistemi gerçekten makroları benimsedi. Topluluğun üyeleri, verileri serileştirebilen ve seri durumdan çıkarabilen, otomatik olarak SQL oluşturabilen ve hatta kodda bırakılan ek açıklamaları başka bir programlama diline dönüştürebilen araçlar da dahil olmak üzere inanılmaz derecede harika kitaplıklar, kavram kanıtları ve makrolarla özellikler oluşturdu. Derleme zamanı.
Julia'nın metaprogramlaması daha anlamlı ve özgür olsa da, Rust, dil genelinde yoğun bir şekilde öne çıktığı için, metaprogramlamayı yükselten modern bir dilin muhtemelen en iyi örneğidir.
Geleceğe Bakış
Şimdi programlama dilleriyle ilgilenmek için inanılmaz bir zaman. Bugün, C++ ile bir uygulama yazıp bir web tarayıcısında çalıştırabiliyorum ya da bir masaüstü veya telefonda çalıştırmak için JavaScript ile bir uygulama yazabiliyorum. Giriş engelleri hiç bu kadar düşük olmamıştı ve yeni programcılar daha önce hiç olmadığı kadar bilgi parmaklarının ucunda.
Bu programcı seçimi ve özgürlüğü dünyasında, bilgisayar bilimi tarihinden ve önceki programlama dillerinden özenle seçilmiş özellik ve kavramlara sahip zengin, modern dilleri kullanma ayrıcalığına giderek daha fazla sahibiz. Bu gelişme dalgasında makroların toplandığını ve tozlandığını görmek heyecan verici. Rust ve Julia onları makrolarla tanıştırırken yeni nesil geliştiricilerin ne yapacağını görmek için sabırsızlanıyorum. Unutmayın, "veri olarak kodlayın" sadece bir slogandan daha fazlasıdır. Herhangi bir çevrimiçi toplulukta veya akademik ortamda metaprogramlamayı tartışırken akılda tutulması gereken temel bir ideolojidir.
'Veri olarak kodla' sadece bir slogandan daha fazlasıdır.
Metaprogramming'in 64 yıllık tarihi, bugün bildiğimiz şekliyle programlamanın gelişiminin ayrılmaz bir parçası olmuştur. Araştırdığımız yenilikler ve tarih, metaprogramlama destanının sadece bir köşesi olsa da, modern metaprogramlamanın sağlam gücünü ve faydasını gösterirler.