Ders 07 / 09
Dinamik Yöntem Tanımı
define_method ile çalışma zamanında yöntem üretmenin, bu yöntemlerin parametre denetiminin ve üretimin yazanı nasıl sınıflandırdığının ölçümü.
Yayın Güncelleme
Bu derste
- define_method ile bir bloktan yöntem tanımlar
- Üretilen yöntemin def ile yazılandan hangi sorularda ayrıldığını iz etiketiyle ölçer
- method_added kancasının def, define_method ve attr_reader için aynı biçimde tetiklendiğini gösterir
- class_eval ile dizgiden üretilen yöntemin yazanını sınıflandırır
İçindekiler
Önceki ders yansımayı verdi: bir nesneye yöntemlerini, durumunu ve türünü sormak. Nokta#x yöntemini Struct.new yazmıştı ve yazan yardımcısı ona kod demişti. Orada üretim bir kara kutuydu; yalnız sonuca baktık. Bu ders üretimin kendisini yapar. Dinamik yöntem tanımı (dynamic method definition), bir yöntemin def yazılarak değil, program çalışırken ad ve gövde verilerek tanımlanmasıdır. Ruby’de bunun aracı define_method‘dur. M11/K01, C’de bir kararın hangi aşamada verildiğini ölçmüştü; burada tanımın ne zaman oluştuğu sorulmuyor, soru yine kimin yazdığıdır.
Eksen aynı: bu yöntemi kim yazdı? Bu dersin yazanı kod. Programcı bir kez döngü yazar, döngü iki yöntemi yazar. Programcının elinden çıkan yalnız define_method çağrısıdır, tek tek yöntem gövdeleri değil. Dört yazanı hatırlayalım: programcı def ile yazar, ata bir üst sınıf ya da modüldür, kod bir üretim çağrısıdır, kimse ise o ada tanım olmamasıdır. Dinamik tanım, kodun yollarından biridir. Neden gerektiği de açıktır: aynı kalıpta onlarca yöntem gerekiyorsa, onları tek tek yazmak yerine kalıbı bir kez yazıp adları bir listeden almak hem kısadır hem de listeye bir ad eklendiğinde yöntemin kendiliğinden doğmasını sağlar. Bu derste o kısalığın bedelini ölçeceğiz: kaynakta aranan bir yöntem adı, kaynakta hiç geçmeyebilir. Aşağıdaki yardımcı önceki dersle aynıdır ve olduğu gibi verilir.
# yazan.rb — bir yontemi kimin yazdigini siniflandiran yardimci
# Siniflandirma sirayla sorar: once alicinin zincirinde bir tanim var mi,
# sonra tanimi alicinin kendi sinifi mi tasiyor, sonra tanim nasil uretildi.
def yazan(alici, ad)
zincir = alici.singleton_class
unless zincir.method_defined?(ad) || zincir.private_method_defined?(ad)
return ["kimse", "tanim yok"]
end
tanim = zincir.instance_method(ad)
sahip = tanim.owner
yer = sahip == zincir ? "tekil sinif" : sahip.to_s
return ["ata", yer] unless sahip == alici.class || sahip == zincir
iz = RubyVM::InstructionSequence.of(tanim)
return ["kod", "#{yer}, ruby govdesi yok"] if iz.nil?
return ["kod", "#{yer}, blok govdesi"] if iz.label.start_with?("block")
["programci", "#{yer}, def govdesi"]
end
Bu blok kaynaktır; dosya diske yazılır ve require_relative ile çekilir.
Ölçülen Sınıflar
Bu dersin gözlemi iki sınıfa dayanır. Hesap, def ile yazılmış bir yöntemi karşılaştırma için sağlar. Ayar ise yöntemlerini bir döngüden alır. Ortak tanımların kesiti şudur.
# yazanlar.rb — bu dersin gozlemi icin gereken tanimlar
require_relative "yazan"
module Raporlanir
def rapor = "rapor: #{bakiye_metni}"
end
class Hesap
include Raporlanir
def initialize(sahip, bakiye)
@sahip = sahip
@bakiye = bakiye
end
def bakiye_metni = "#{@sahip} #{@bakiye}"
def net = @bakiye - komisyon
private
def komisyon = 2
end
class Ayar
%i[renk boyut].each do |ad|
define_method(ad) { "#{ad} varsayilan" }
end
end
Bu blok de kaynaktır. Ayar tanımında %i[renk boyut] iki Symbol değerinden oluşan bir dizi kurar; each her ad için define_method çağırır ve bloğu o adın yöntemi yapar. Ayar sınıfında renk ve boyut adında bir def yoktur.
Üretilen Yöntemi Kim Yazdı
Bu dersin gözlemi Ayar nesnesinin renk yöntemidir. Aşağıdaki dosya yardımcıyı çağırır ve yöntemin ürettiği değeri yanına basar.
# uretilen.rb — Ayar#renk yontemini kim yazdi require_relative "yazanlar" a = Ayar.new p yazan(a, :renk) puts a.renk
["kod", "Ayar, blok govdesi"] renk varsayilan
Yazan kod. Yöntemin sahibi Ayar’ın kendisi çıktı, yani ata değil; ama gövde bir def değil, define_method’a verilen bir blok. Yardımcı bu ayrımı iz etiketinin block ile başlamasından okudu. Önceki dersteki Nokta#x da koddu, ama onun gövdesi hiç yoktu: orada cevap ruby govdesi yok idi, burada blok govdesi. Aynı kategoriye iki ayrı biçim düşer. MP10. Yalnız renk yöntemi ölçüldü; boyut aynı döngüden geldiği için aynı sonucu verecektir, ama bu bir varsayımdır.
Üretim İzi
Yöntem nesnesinin gövdesinin bir iz etiketi (label) vardır; Ruby bunu derlenmiş gövdeye yazar. Yardımcı etikete bakıp block ile başlayıp başlamadığını sorar. Etiketi doğrudan basarak iki yöntemi karşılaştırırız: döngüyle üretilen renk ve def ile yazılan net.
# iz.rb — iki yontemin gövde etiketi require_relative "yazanlar" p Ayar.instance_methods(false) iz = RubyVM::InstructionSequence puts iz.of(Ayar.instance_method(:renk)).label puts iz.of(Hesap.instance_method(:net)).label
[:boyut, :renk] block (2 levels) in <class:Ayar> net
instance_methods(false) yalnız Ayar’ın kendi herkese açık yöntemlerini listeledi. Etiketler farkı gösterir: renk için block (2 levels) in <class:Ayar>, net için net. İki seviye, tanımın iki blok içinde olduğunu söyler: biri each bloğu, öteki define_method bloğu. net‘in etiketi ise yöntemin kendi adıdır. Yardımcının start_with?("block") sorusu bu iki etiketi ayırır. MP11. Etiket biçimi Ruby 4.0.7’de ölçüldü; başka bir sürümde metin değişebilir, ama block ile başlama kuralı bu ölçümden çıkarılan tek şeydir.
Çalışırken Yöntem Eklemek
define_method bir ad ve bir blok alır, yöntemi o ada bağlar ve adı Symbol olarak döndürür. Yöntem aynı satırdan sonra çağrılabilir. Sınıf zaten yüklenmişken de çalışır: sınıfa yeni bir yöntem eklemek için class anahtar sözcüğüyle yeniden açmak gerekmez.
# tanim.rb — define_method ile yontem eklemek ve kaldirmak
require_relative "yazanlar"
donus = Ayar.define_method(:tema) { "koyu" }
p donus
p Ayar.new.tema
p yazan(Ayar.new, :tema)
Ayar.remove_method(:tema)
begin
Ayar.new.tema
rescue NoMethodError => e
puts e.class
end
:tema "koyu" ["kod", "Ayar, blok govdesi"] NoMethodError
Dönüş :tema oldu; yeni yöntem hemen cevap verdi ve yazan onu kod olarak sınıflandırdı, tıpkı döngüyle üretilenler gibi. Üretimin zamanı değişti, tanımın biçimi değişmedi: gövde yine bir blok. remove_method yöntemi Ayar’ın kendi tablosundan sildi ve ardından aynı ileti NoMethodError verdi. Silinen yöntem, aynı adda bir ata tanımı olsaydı, o ata tanımı yeniden görünür olurdu; remove_method yalnız sınıfın kendi tanımını siler. MP12. remove_method yalnız bu sınıfta ve ata tanımı olmayan bir adla ölçüldü; ata tanımı olan durum burada gösterilmedi.
Adı Çalışırken Belirlemek
def ile yazılan yöntemin adı kaynakta sabittir. define_method ise adı bir dizgi ya da Symbol olarak alır; ad bir değişkenden ya da hesaplamadan gelebilir. Aşağıdaki sınıf iki alan adı için birer okuyucu ve yazıcı üretir; ürettiği yöntem adlarını kaynakta hiçbir yerde düz yazı olarak taşımaz.
# form.rb — adi hesaplanan yontemler
require_relative "yazanlar"
class Form
%w[ad soyad].each do |alan|
define_method("#{alan}_yaz") { |deger| instance_variable_set("@#{alan}", deger) }
define_method("#{alan}_oku") { instance_variable_get("@#{alan}") }
end
end
f = Form.new
f.ad_yaz("ece")
p f.ad_oku
p f.soyad_oku
p f.instance_variables
p Form.instance_methods(false).sort
p yazan(f, :ad_yaz)
"ece" nil [:@ad] [:ad_oku, :ad_yaz, :soyad_oku, :soyad_yaz] ["kod", "Form, blok govdesi"]
Dört yöntem tek bir döngüden çıktı: ad_oku, ad_yaz, soyad_oku ve soyad_yaz. Yazılmış alan okunduğunda "ece" geldi; yazılmamış alan nil verdi, çünkü o örnek değişkeni hiç atanmadı. instance_variables yalnız @ad’ı listeledi: ikinci alanın örnek değişkeni, ona ilk değer yazılana kadar var değildir. Yazan yine kod çıktı. Burada programcı yalnız bir kalıp yazdı; kalıbın dört örneğini kod üretti. MP13. Bu sınıf iki alanla ölçüldü; ad dizgisinin yöntem adı olarak geçersiz karakterler içermesi durumu sınanmadı.
Parametre Denetimi
Blok normalde gevşek davranır: eksik argüman nil olur. define_method’a verilen blok ise yöntem gibi davranır ve argüman sayısını denetler. Aşağıdaki ölçüm bunu iki yoldan sınar: bloğu doğrudan vermek ve bir proc nesnesini & ile vermek.
# parametre.rb — define_method bloklari ve argüman denetimi
class Ayar
define_method(:carp) { |n| n * 2 }
define_method(:pcarp, &proc { |n| n * 2 })
end
a = Ayar.new
p Ayar.instance_method(:carp).arity
p a.carp(4)
p proc { |n| n }.call
[:carp, :pcarp].each do |ad|
begin
a.public_send(ad)
rescue ArgumentError => e
puts "#{ad} -> #{e.class}"
end
end
1 8 nil carp -> ArgumentError pcarp -> ArgumentError
carp için argüman sayısı 1 çıktı: üretilen yöntem, bloğun parametresini zorunlu bir parametre olarak taşıyor. carp(4) 8 verdi. Üçüncü satır karşıtı gösterir: argümansız çağrılan sıradan bir proc hata vermedi, nil döndürdü. Aynı biçimdeki blok define_method içinde ise ArgumentError verdi; proc nesnesini & ile geçirmek bunu değiştirmedi, pcarp da hata verdi. Yani üretim anında blok yöntem kuralına geçer. MP14. Yalnız tek zorunlu parametreli bloklar ölçüldü; varsayılan değerli ve değişken argümanlı bloklar bu derste sınanmadı.
Bloğun Gördüğü Değişkenler
define_method ile def arasındaki en kullanışlı fark, bloğun çevresindeki yerel değişkenleri görmesidir. Kapanış (closure), bir bloğun tanımlandığı yerdeki yerel değişkenleri sonradan çağrıldığında da taşımasıdır. def yeni bir kapsam açar ve çevresindeki yerel değişkenleri görmez; blok ise görür.
# sayac.rb — define_method blogu cevresindeki degiskeni gorur
class Sayac
adet = 0
define_method(:say) { adet += 1 }
def bak = defined?(adet).inspect
end
a = Sayac.new
b = Sayac.new
p [a.say, a.say, b.say]
puts a.bak
[1, 2, 3] nil
Üç çağrı [1, 2, 3] verdi. adet sınıf gövdesinde bir yerel değişkendi ve say bloğu onu kapanış olarak taşıdı. Sayaç iki nesne arasında paylaşıldı: ikinci nesnenin ilk çağrısı 3 oldu, çünkü değişken nesnede değil, bloğun kapanışında duruyor. def ile yazılan bak ise aynı adı göremedi; defined?(adet) orada nil döndü ve inspect bunu "nil" olarak yazdırdı. Bu yüzden define_method ile üretilen bir yöntemin durumu örnek değişkenlerinde olmayabilir; yansıma ile instance_variables çağrısı bu sayacı göstermez. MP15. Bloğun kapanışı yalnız bu sınıf gövdesindeki tek yerel değişkenle ölçüldü; kapanışın başka yöntemler arasında paylaşılması bu örnekte gösterilmedi.
Kanca
Bir sınıfa yeni bir yöntem eklendiğinde Ruby sınıfın method_added yöntemini çağırır. Kanca (hook), belirli bir olay olduğunda çalışması için tanımlanan yöntemdir. Aşağıdaki sınıf bu kancayı yazar ve eklenen adları bir listede toplar. Üç ayrı yolla yöntem eklenir: def, define_method ve attr_reader.
# kanca.rb — method_added hangi yollarda tetiklenir
class Kumbara
@eklenen = []
class << self
attr_reader :eklenen
def method_added(ad)
@eklenen << ad
end
end
def a = 1
define_method(:b) { 2 }
attr_reader :c
end
p Kumbara.eklenen
[:a, :b, :c]
Üç yol da kancayı aynı biçimde tetikledi: [:a, :b, :c]. Kanca yöntemi hangi yolla üretildiğini bilmez, yalnız adı alır. Bu, önceki derslerin gözlemiyle birlikte okunmalıdır: kanca tetiklemesi yazanı söylemez. Yazanı söyleyen şey, tanımın gövdesinin biçimidir; kanca bunu görmez. Bu yüzden kanca yöntem eklenen yeri izlemek için iyidir, ama kim yazdı sorusuna cevap vermez. MP16. Kanca yalnız bu sınıfta, sınıf gövdesi çalışırken ölçüldü.
Erişimci de Kod Üretir
attr_accessor, aynı adla bir okuyucu ve bir yazıcı yöntem üretir. Erişimci (accessor) bu iki yöntemin birlikte adıdır; yalnız okuyucuya attr_reader, yalnız yazıcıya attr_writer denir.
# erisim.rb — attr_accessor iki yontem uretir require_relative "yazan" class Erisim attr_accessor :renk end p Erisim.instance_methods(false).sort p yazan(Erisim.new, :renk) p yazan(Erisim.new, :renk=)
[:renk, :renk=] ["kod", "Erisim, ruby govdesi yok"] ["kod", "Erisim, ruby govdesi yok"]
İki yöntem çıktı: renk ve renk=. İkisinin de yazanı kod, ama gövde biçimi define_method’dan farklı: etiket yok, çünkü attr_accessor Ruby gövdesi bırakmaz. Erisim sınıfında hiçbir def yazılmadı. blok govdesi ile ruby govdesi yok kodun iki alt biçimidir. MP17. Yalnız attr_accessor ölçüldü; attr_writer tek başına ölçülmedi.
Sınırlayıcı Ölçüm
Şimdiye kadar kod, define_method ya da attr_* çağrısı demekti. Bir üretim biçimi daha var: sınıfa bir dizgiyi Ruby kodu olarak yorumlatmak. class_eval bir dizgiyi sınıfın bağlamında çalıştırır. Aşağıdaki ölçüm aynı işi iki yolla yapar: biri dizgiyle, öteki blokla.
# sinir.rb — ayni is, iki uretim yolu
require_relative "yazanlar"
Ayar.class_eval "def dizgiden = 4"
Ayar.define_method(:blokla) { 4 }
a = Ayar.new
p [a.dizgiden, a.blokla]
p yazan(a, :dizgiden)
p yazan(a, :blokla)
[4, 4] ["programci", "Ayar, def govdesi"] ["kod", "Ayar, blok govdesi"]
İki yöntem de 4 verdi, ama yazanları ayrıldı. Dizgiden üretilen yöntem çalışma zamanında yaratıldığı hâlde yazan onu programcı olarak sınıflandırdı, çünkü dizgi içinde bir def gövdesi var. define_method ile üretilen ise kod. Kural biçimi okur, niyeti okumaz: kategori üretimin ne zaman olduğunu değil, tanımın hangi biçimde olduğunu ölçer. Bu yüzden kod üretimi her zaman koda düşmez. Tersi de doğrudur: kaynakta define_method görmek, yazanın kod olduğunu söylemez; bir dizginin içinde yazılmış def ise çalışma zamanında üretilmiş olsa bile programcıya düşer. Yansıma yöntemin sahibini ve gövdesinin biçimini görür, üretimi başlatan çağrıyı görmez. Bu yüzden üretimin izini sürmek, etikete bakmaktan çok kaynakta define_method ve class_eval çağrılarını aramaktır. MP18. Dizgiden üretim yalnız basit tek satırlık def ile ölçüldü; daha karmaşık dizgilerde etiket ve gövde biçimi farklı olabilir.
Özet
define_methodbir ad ve bir blok alır, yöntemi o ada bağlar ve adıSymbololarak döndürür; yeni yöntem hemen çağrılabilir.- Üretilen yöntemin yazanı koddur: gövdesi bir bloktur ve iz etiketi
blockile başlar.defile yazılanın etiketi yöntemin adıdır. define_methodbloğu argüman sayısını yöntem gibi denetler; sıradanprocdenetlemez.method_addedkancasıdef,define_methodveattr_readeriçin aynı tetiklenir; kanca yazanı söylemez.attr_accessoriki yöntem üretir ve ikisinin de yazanı koddur, ama gövde biçimi farklıdır.- Dizgiden
class_evalile üretilendefyazanı programcı çıkar: kategori tanımın biçimini ölçer, üretim anını değil.
Sonraki Adım
Bu derste yöntemleri biz tanımladık, çağrılmadan önce. Sıradaki ders tersini ölçer: çağrılan ada hiçbir tanım yoksa Ruby ne yapar ve bu boşluğu bir sınıf kendisi nasıl doldurabilir. O derste yazanın dördüncü kategorisi, kimse, ortaya çıkar.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.