Ders 01 / 12
Ruby'nin Tasarım Felsefesi
Ruby'de 5 + 3, x = 5, bir dizinin map'i ve 5.between?(1, 10) aynı biçimde yazılır; sonucu üreten kod ise dört ayrı yerde durur. Ders bu dört yanıtlayanı on iki gözlemle ölçer ve okunabilirliği önceleyen tasarımın işleçleri bile yönteme çevirdiğini gösterir.
Yayın Güncelleme
Bu derste
- İşleçlerin ve okunur yöntem adlarının aynı düzenekle, bir iletiyle çalıştığını koşumla gösterir
- Bir iletinin sınıf, modül, blok ya da sözdizimi tarafından yanıtlandığını kurala göre sınıflandırır
- Bir modülün yeteneği alıcının sınıfındaki bir yönteme dayandırdığını sahip ölçümüyle ayırt eder
İçindekiler
M12 PHP ile kapandı. Orada bir sorunun hangi kanaldan duyulduğu, bir değişikliğin nereye ulaştığı ve bir parçanın bir istekte koşup koşmadığı ölçüldü. Ruby müfredatı başka bir sorudan başlar. 5 + 3, x = 5, [1, 2, 3].map { ... } ve 5.between?(1, 10) ekranda aynı biçimde yazılır: bir değer, bir nokta ya da işleç, bir ad. Ama bu dört ifadenin sonucunu üreten kod dört ayrı yerde durur. Bu kurs her düzeneği tek bir soruyla ölçer: bu iletiyi kim yanıtlıyor?
İleti (message), bir nesneye gönderilen ad ve argümanlardır; iletiyi alan nesneye alıcı (receiver), iletiye karşılık çalışan koda yöntem (method) denir. Programlama Temelleri kursunda (M01/K02) değişken, tip ve fonksiyon kavramları kurulmuştu; burada yeni olan, bu kavramların Ruby’de hangi düzenekle verildiğidir. Bu ders düzeneğin dilini ve ölçünün çekirdeğini kurar, sonraki beş ders aynı çekirdekten birer kesit alır.
Dört Yanıtlayan
Bir iletiyi karşılayan kodun bulunabileceği dört yer vardır. Kurs boyunca bu dört ad değişmez; kategorinin üst adı yanıtlayandır. “Sınıf” sözcüğü Ruby’nin Class’ıyla çakıştığı için ölçü kategorisi yazılırken “yanıtlayan sınıf” ya da “sınıf yanıtlayanı” denir; çıplak “sınıf” yalnız Ruby sınıfıdır.
| Yanıtlayan | Tanım |
|---|---|
| sınıf | İletiyi karşılayan yöntem, alıcının ait olduğu bir sınıfta tanımlıdır |
| modül | Yöntem bir modülde (module) tanımlıdır ve sınıfa karıştırılmıştır |
| blok | Yöntem çağıranın verdiği bloğa dayanır; aynı ileti başka blokla başka sonuç verir |
| sözdizimi | Ortada ileti yoktur; ifadeyi dilin kendisi işler |
Bu ayrım önceki müfredatlardaki bir soruya benzer görünür ama aynı değildir. M08/K03’te bir davranışın kimin sözü olduğu soruldu: dil mi, kitaplık mı, programcı mı vaat ediyor. Burada söz aranmaz, kod aranır; ölçülen, davranışı vaat eden taraf değil, iletiyi o anda karşılayan tanımdır.
DT1. Bu derste “yanıtlayan”, iletiyi karşılayan yöntemin tanımının bulunduğu yerdir. Yöntemin hangi sırayla aranıp bulunduğu bu kursta ölçülmez; yalnız sonuç, yani sahip ölçülür. Arama sırası müfredatın bir sonraki kursunun konusudur.
Okunabilirlik Önceliği
Ruby’nin tasarım tercihi, kodun bir cümle gibi okunmasını işleç ekonomisinin önüne koymaktır. Bu tercih en açık biçimde adlandırılmış yöntemlerde görülür: 5.between?(1, 10) ifadesi okunduğu gibi bir sorudur. İkinci sonuç daha az görünürdür: işleçler de yöntemdir. Aşağıdaki dosya aynı toplamı üç yazımla verir.
# isleclar.rb — isleclerin de yontem oldugunu gosterir puts 1 + 2 puts 1.+(2) puts 5.send(:+, 3) puts 5.between?(1, 10) puts 5.clamp(1, 3)
3 3 8 true 3
1 + 2 ile 1.+(2) aynı sonucu verir, çünkü ilki ikincinin kısa yazımıdır: alıcı 1, ileti +, argüman 2. send iletinin adını sembol olarak alır ve 5.send(:+, 3) aynı iletiyi gönderir. Böylece 5 + 3, x.upcase ve 5.between?(1, 10) arasında yazım farkı kalsa da düzenek farkı kalmaz: üçü de bir alıcıya gönderilen birer iletidir. Bu, sonraki derslerin dayandığı sezgidir; her işlemin bir ileti olduğu bir dilde “kim yanıtlıyor” sorusu anlamlı olur.
DT2. Ölçümler Ruby 4.0.7’de koşturuldu. Bir yöntemin hangi sınıfta ya da modülde tanımlı olduğu sürümden sürüme taşınabilir; derste yazılan sahip adları bu sürümün sonucudur, dilin sözü değildir.
Aynı İşin İki Yazımı
Okunabilirlik tercihi yalnız yöntem adlarında değil, aynı işin birden çok yazıma sahip olmasında da görünür. Ruby’de bir işi görmenin tek yolu yoktur ve yazarın seçtiği yazım, okuyana işin amacını ne kadar açık söylediğini belirler. Aşağıdaki iki dosya bu tercihin ölçülebilir kısmını sorar: aynı işin iki yazımı yalnız görünüşte mi ayrılır, yoksa farklı bir yöntemi mi çağırır? Ölçü ekseni yanıtlayandır; her yazımın iletisini kimin karşıladığı, mümkün olan yerde, çıktının yanında basılır.
# iki_yazim.rb — ayni isin iki yazimi ve yanitlayanlari
dizi = [4, 5, 6]
sayac = 0
dizi.each { |n| sayac += n }
puts sayac
puts dizi.sum
puts dizi.method(:each).owner
puts dizi.method(:sum).owner
x = 7
puts "kucuk" unless x > 10
puts "kucuk" if !(x > 10)
15 15 Array Array kucuk kucuk
Sayaçlı yazım ile sum aynı toplamı, 15’i verdi. İkisinin yanıtlayanı da Array çıktı: each de sum da dizinin sınıfında tanımlıdır. Yani iki yazım arasındaki fark yanıtlayanda değil, okumadadır. Sayaçlı yazımda bir değişkenin açılması, bir bloğun yazılması ve bloğun içinde değişkenin güncellenmesi birlikte okunur; sum yazımında ise niyet tek adla söylenir. unless ile olumsuzlamalı if ise aynı satırı iki kez bastırdı. Burada yanıtlayan sorusu farklı bir cevap taşır: unless ve if anahtar sözcüktür, bir yöntem değildir; bu yüzden bu iki satırın yanıtlayanı olarak dilin sözdizimi sayılır. Koşulun içindeki x > 10 ise bir iletidir; sahibi bu dosyada basılmadı. Olumsuzlamalı yazımdaki olumsuzlama işleci de ayrı bir iletidir ve unless yazımında bulunmaz; onun sahibi de burada ölçülmedi. Olumsuz bir koşulu tersine çevirip olumsuzlamalı if ile yazmak da çalışır, ancak okuyan iki ayrı işlem, önce karşılaştırmayı, sonra olumsuzlamayı zihninde birleştirmek zorunda kalır.
# parantez.rb — parantezli ve parantezsiz cagri ayni iletidir liste = [3, 1, 2] a = liste.push 9 b = liste.push(9) puts a.equal?(b) puts liste.size puts liste.method(:push).owner puts liste.sort.first puts liste.sort().first() puts method(:puts).owner
true 5 Array 1 1 Kernel
Parantezsiz liste.push 9 ile parantezli liste.push(9) aynı diziyi döndürdü: equal? true verdi. İki çağrı da aynı nesneyi geri verdi ve dizi iki kez büyüyerek 5 elemana ulaştı. push iletisinin yanıtlayanı Array‘dir. sort.first ile sort().first() aynı 1 değerini verdi, ve puts çağrısının sahibi Kernel çıktı. İki yazım çiftinin hiçbiri ayrı bir düzenek değildir: parantez yalnız argümanların nerede bittiğini işaretler, ne alıcıyı ne de iletiyi değiştirir. Bu ölçümden çıkan kural şudur: Ruby’de okunabilirlik, yazarın elindeki bir serbestliktir ve serbestlik düzeneği değiştirmez. Aynı işi görmek için seçilen yazım, makinenin değil okuyanın işini kolaylaştırmalıdır.
DT3. Hangi yazımın daha çabuk okunduğu ölçülmedi. Ölçülen yalnızca, iki yazımın aynı sonucu verdiği ve yanıtlayanın değişmediğidir; okunabilirlik yargısı ekibin yazım tercihine bağlıdır ve bu kursun savı değildir.
Okunabilirliğin Sınırı
Okunabilirlik önceliği bir serbestlik tanır, ama her serbestlik iletiyi değiştirmeden yazılamaz. Bu bölümün sorusu şudur: yazımın değiştiği yerde ileti de değişir mi? İki yazım yan yana konunca bazen ileti aynı kalır, bazen de yazım farkı ayrı bir iletiye karşılık gelir. Ölçü yanıtlayan ve sonuçtur; dosya kullanılan dört iletinin sahibini basar.
# yazim_sinir.rb — yazim degisince ileti degisir mi a = 2 b = 3 puts a + b * 2 toplam = (a + b) * 2 puts toplam == 10 puts a.+(b).*(2) puts a.method(:+).owner puts a.method(:*).owner puts 10.fdiv(4) puts 10 / 4 puts 10.method(:fdiv).owner puts 10.method(:/).owner
8 true 10 Integer Integer 2.5 2 Integer Integer
a + b * 2 ifadesi 8 verdi; çarpma toplamadan önce yapıldı. (a + b) * 2 ise 10 verdi ve toplam == 10 karşılaştırması true oldu. Üçüncü satırdaki a.+(b).*(2) yazımı da 10 verdi: alıcı a, ileti +, argüman b, ardından sonuca * iletisi gönderildi. Yani yazarın parantezle yaptığı öncelik seçimi, noktalı yazımda iletilerin zincirleme sırasıyla birebir aynı sonucu verir. İşleç öncelikleri bu yüzden bir yanıtlayan sorusu değildir; hangi iletinin hangi alıcıya gideceğini belirleyen bir yazım kuralıdır. Sahip satırları iki işlecin de Integer sınıfında tanımlı olduğunu gösterdi.
Okunabilirlik açısından ilginç olan, iki bölme yazımıdır. 10.fdiv(4) 2.5, 10 / 4 ise 2 verdi. İkisinin de yanıtlayanı Integer çıktı, ama sonuçlar farklıdır: / tam sayı bölmesi yapar, fdiv kesirli sonuç verir. Yazım farkı burada aynı işin iki biçimi değil, iki ayrı iletidir. Ruby’nin tasarımı bu ayrımı adla yapar: kesirli sonuç isteyen kişi fdiv yazar ve okuyan niyeti adından anlar. Bu da bölümün sonucudur: yazım serbestliği, adlandırılmış iletilerde güvenle kullanılır; işleçlerde ise sonucun aynı kalıp kalmadığı ölçülerek bilinir.
DT4. Tam sayı bölmesinin Integer için 2 verdiği bu sürümde ölçüldü. Bölme davranışının başka sayı türlerinde de aynı olduğu ölçülmedi.
Çekirdek: Yardımcı ve On İki Gözlem
Kurs iki dosyadan oluşan bir ölçüm çekirdeği kullanır. İlki, bir iletiyi sınıflandıran yardımcıdır ve kural olarak yazılmıştır: etiket değil, soru sırası karar verir. Önce defined? ifadenin bir yöntem çağrısı olup olmadığını söyler; değilse yanıtlayan sözdizimidir. Sonra aynı ileti iki farklı blokla iki farklı sonuç veriyorsa yanıtlayan bloktur. Son olarak yöntemin sahibi bir Class örneğiyse sınıf, değilse modüldür.
# yanit.rb — bir iletiyi kimin yanitladigini siniflandiran yardimcilar
# Siniflandirma sirayla sorar: once ifade bir ileti mi (defined?),
# sonra iki farkli blok iki farkli sonuc veriyor mu, sonra yontemin sahibi kim.
def yanitlayan(tanim, sahip: nil, sonuc: nil, bloklu: nil)
return ["sozdizimi", "defined? #{tanim}"] unless tanim == "method"
if bloklu
birinci, ikinci = bloklu.call
return ["blok", "#{birinci.inspect} / #{ikinci.inspect}"] unless birinci == ikinci
end
tur = sahip.instance_of?(Class) ? "sinif" : "modul"
[tur, "#{sahip}, sonuc #{sonuc.inspect}"]
end
Bu dosyanın çıktı bloğu yoktur: tek başına bir şey yazdırmaz, yalnız diğer dosyanın çekeceği bir yardımcıdır. İkinci dosya on iki gözlemi yapar. Her gözlem bir lambda içindedir; lambda çağrılınca yanitlayan yardımcısına iletinin defined? sonucunu, yöntemin sahibini ve sonucunu verir. Dosya çalışma dizinine bir not.txt yazar, çünkü son gözlem onu okur.
# yanitlayan.rb — on iki gozlem: bu iletiyi kim yanitliyor
require_relative "yanit"
File.write("not.txt", "ilk satir\nikinci satir\n")
sayilar = [1, 2, 3]
gozlemler = {
"okunur karsilastirma" => -> { yanitlayan(defined?(5.between?(1, 10)), sahip: 5.method(:between?).owner, sonuc: 5.between?(1, 10)) },
"tam sayi toplami" => -> { yanitlayan(defined?(5 + 3), sahip: 5.method(:+).owner, sonuc: 5 + 3) },
"yerel atama" => -> { yanitlayan(defined?(x = 5)) },
"aralik eslesmesi" => -> { yanitlayan(defined?((1..10) === 7), sahip: (1..10).method(:===).owner, sonuc: (1..10) === 7) },
"alicisiz cagri" => -> { yanitlayan(defined?(format("%03d", 7)), sahip: method(:format).owner, sonuc: format("%03d", 7)) },
"sembole donusum" => -> { yanitlayan(defined?("ad".to_sym), sahip: "ad".method(:to_sym).owner, sonuc: "ad".to_sym) },
"varsayilan deger" => -> { yanitlayan(defined?(Hash.new), bloklu: -> { [Hash.new { 0 }[:x], Hash.new { |_, k| k.to_s }[:x]] }) },
"eleman donusumu" => -> { yanitlayan(defined?(sayilar.map), bloklu: -> { [sayilar.map { |n| n * 2 }, sayilar.map { |n| n.to_s }] }) },
"dilimleme" => -> { yanitlayan(defined?((1..5).each_slice(2)), sahip: (1..5).method(:each_slice).owner, sonuc: (1..5).each_slice(2).to_a) },
"lambda cagrisi" => -> { yanitlayan(defined?(->(n) { n * 2 }.call(3)), sahip: ->(n) { n * 2 }.method(:call).owner, sonuc: ->(n) { n * 2 }.call(3)) },
"kurtarma" => -> { yanitlayan(defined?(begin; Integer("x"); rescue ArgumentError; 0; end)) },
"dosya okuma" => -> { yanitlayan(defined?(File.open("not.txt")), bloklu: -> { [File.open("not.txt") { |f| f.gets.chomp }, File.open("not.txt") { |f| f.readlines.size }] }) },
}
adet = { "sinif" => 0, "modul" => 0, "blok" => 0, "sozdizimi" => 0 }
printf("%-22s%-11s%s\n", "gozlem", "yanitlayan", "kanit")
gozlemler.each do |ad, gozlem|
tur, kanit = gozlem.call
adet[tur] += 1
printf("%-22s%-11s%s\n", ad, tur, kanit)
end
puts
adet.each { |tur, n| printf("%-11s%d\n", tur, n) }
puts "toplam gozlem: #{adet.values.sum}"
gozlem yanitlayan kanit okunur karsilastirma modul Comparable, sonuc true tam sayi toplami sinif Integer, sonuc 8 yerel atama sozdizimi defined? assignment aralik eslesmesi sinif Range, sonuc true alicisiz cagri modul Kernel, sonuc "007" sembole donusum sinif String, sonuc :ad varsayilan deger blok 0 / "x" eleman donusumu blok [2, 4, 6] / ["1", "2", "3"] dilimleme modul Enumerable, sonuc [[1, 2], [3, 4], [5]] lambda cagrisi sinif Proc, sonuc 6 kurtarma sozdizimi defined? expression dosya okuma blok "ilk satir" / 2 sinif 4 modul 3 blok 3 sozdizimi 2 toplam gozlem: 12
On iki gözlemin 4’ünü bir sınıf, 3’ünü bir modül, 3’ünü bir blok yanıtladı; 2’sinde ileti yoktu, ifadeyi sözdizimi işledi. Bu sayılar kursun bütün dersleri için bağlayıcıdır: sonraki dersler bu sayıları değiştirmez, her biri tablodan kendi satırını alıp ayrıntısında ölçer.
Tablodan dört okuma çıkar. Sınıf yanıtlayanı en dolaysız olanıdır: 5 + 3’teki + bir işleç gibi görünür ama Integer sınıfında tanımlı bir yöntemdir; "ad".to_sym String‘de, lambda’nın call’ı Proc’ta tanımlıdır. Modül yanıtlayanı sınıfın kendisinde bulunmayan bir yeteneği gösterir: between? Integer’da değil Comparable’dadır, each_slice Range’te değil Enumerable’dadır, alıcısız format Kernel’dedir. Blok yanıtlayanı sonucu çağırana bırakır: Hash.new, map ve File.open aynı iletiyle iki sonuç verdi ve fark yalnız verilen bloktaydı. Sözdizimi yanıtlayanında alıcı da ileti de yoktur: x = 5 için defined? "assignment", begin ... rescue ... end için "expression" döndürdü.
Çıktıda bilerek basılmayan şeyler de vardır: object_id, bellek adresi taşıyan inspect çıktısı ve istisna ileti metni. Bu değerler her koşumda değişir ya da ortama bağlıdır; yerlerine sınıf adı, mantıksal sonuç ve sayı basılır.
Sınırlayıcı Ölçüm
Bu dersin satırı okunur karsilastirma’dır ve yanıtlayanı modüldür: 5.between?(1, 10) iletisini Comparable karşılar. Ancak bu cümle yarım kalırsa yanıltır. Modülün tanımladığı yöntem, karşılaştırmanın kendisini yapmaz; onu alıcıya sorar. between? bir değerin iki sınır arasında olup olmadığını söylemek için <=> iletisini alıcıya gönderir ve karşılaştırmanın nasıl yapılacağı alıcının sınıfındaki <=> yöntemindedir. Sahipleri yan yana basalım.
# karsilastirma.rb — between? Comparable'da, <=> ise alicinin sinifinda
[5, "a", :a].each do |alici|
printf("%-4s between? %-11s <=> %s\n", alici.inspect, alici.method(:between?).owner, alici.method(:<=>).owner)
end
5 between? Comparable <=> Integer "a" between? Comparable <=> String :a between? Comparable <=> Symbol
Üç alıcıda between?’in sahibi aynıdır, <=>’in sahibi her seferinde alıcının kendi sınıfıdır. Aynı modül üç ayrı sınıfa aynı yeteneği verir, çünkü üçü de karşılaştırmayı kendisi yanıtlar. Modül bu yüzden tek başına yanıt vermez: yeteneği modülden gelir, dayanağı sınıftan.
Bu sınır, yanıtlayanı tek bir sahip adına indirgemeyi engeller. Dört yanıtlayanlı sınıflandırma iletiyi karşılayan tanımı bulur; o tanımın başka bir iletiye dayanması, yeni bir ölçüm sorusudur ve çoğu zaman cevabı bir sınıftır.
Özet
- Ruby’de her işlem bir alıcıya gönderilen iletidir;
1 + 2ile1.+(2)aynı iletidir vesendiletiyi adıyla gönderir. - Ölçü sorusu “bu iletiyi kim yanıtlıyor”dur ve dört yanıtlayanı vardır: sınıf, modül, blok, sözdizimi.
- Yanıtlayanı etiket değil kural verir: önce
defined?, sonra iki blokla karşılaştırma, sonra yöntemin sahibi. - Çekirdeğin on iki gözleminde dağılım 4 sınıf, 3 modül, 3 blok ve 2 sözdizimidir.
- Bu dersin yanıtlayanı modüldür:
between?Comparable’dadır, ama karşılaştırmanın kendisi olan<=>alıcının sınıfındadır. - Aynı işin iki yazımı (
eachile sayaç vesum, parantezli ve parantezsiz çağrı) ölçülen sahipleri değiştirmedi;unlessile olumsuzlamalıifaynı sonucu verdi. Okunabilirlik yazarın seçimidir, düzeneği değiştirmez.
Sonraki Adım
5 + 3’ün +’sının Integer‘da tanımlı olması bir şeyi gizli varsayar: 5 bir nesnedir ve bir sınıfa aittir. İlkel değerlerin de nesne olup olmadığı ve alıcı ile yanıtlayanın birbirinden nasıl ayrıldığı sonraki dersin, Nesne Modeli Sezgisi’nin konusudur.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.