Melih Kocatürk

Melih Kocatürk

Code Ninja

Ruby.new

Melih Kocatürk / 21.05.2024

Programlama dilleri üzerine birçok kitap hemen hemen aynı görünür. Temel türlerle ilgili bölümlerle başlarlar: integer, string vb. Daha sonra if ve while ifadelerine ve döngülerine geçmeden önce 2 + 3 gibi ifadelere bakarlar. Daha sonra belki 7. veya 8. Bölüm civarında sınıflardan bahsetmeye başlayacaklar. Bunu biraz sıkıcı buluyoruz.

Bunun yerine, bu kitabı tasarlarken büyük bir planımız vardı. Dili yukarıdan aşağıya, sınıflardan ve nesnelerden başlayarak, en ince ayrıntısına kadar söz dizimi ayrıntılarıyla sona erdirerek belgelemek istedik. O zamanlar iyi bir fikir gibi görünüyordu. Sonuçta Ruby’deki her şey bir nesnedir, bu yüzden önce nesnelerden bahsetmek mantıklıydı.

Ya da biz öyle düşündük.

Ne yazık ki bir dili yukarıdan aşağıya anlatmak çok zor. Eğer dizeleri, if ifadelerini, atamaları ve diğer detayları ele almadıysanız sınıf örnekleri yazmak zordur. Yukarıdan aşağıya açıklamamız boyunca, örnek kodun anlamlı olması için gerekli olan düşük düzeyli ayrıntıları vermeye karar verdik.

Böylece başka bir büyük plan hazırladık (bize boşuna pragmatik demiyorlar). Ruby’yi hâlâ en yukarıdan başlayarak tanımlıyoruz. Ancak bunu yapmadan önce, Ruby’de kullanılan özel sözcüklerin yanı sıra örneklerde kullanılan tüm ortak dil özelliklerini açıklayan kısa bir bölüm ekledik; bu, bizi kitabın geri kalanına yönlendirecek mini bir eğitimdir. Ve bu mini eğitim bu bölümdür.

Ruby Nesne Yönelimli Bir Dildir

Tekrar söyleyelim. Ruby nesne yönelimli bir dildir. Programlama açısından bir nesne, verileri, bu verileri işleyen mantıkla birleştiren bir şeydir. Ve bir dil, nesnelerin yaratılmasını kolaylaştıran dil yapıları sağlıyorsa “nesne yönelimlidir”. Tipik olarak nesne yönelimli diller, nesnelerin, verilerinin ne olduğunu tanımlamasına, işlevlerini tanımlamasına ve diğer nesnelerin bu işlevselliğe erişmesine izin vermek için ortak bir sözdizimi sağlamasına olanak tanır.

Java, JavaScript ve Python gibi diğer nesne yönelimli dillerin aksine, tüm Ruby türleri nesnelerdir ve farklı davranan nesne olmayan temel türler yoktur. Ancak birçok dil nesne yönelimli olduğunu iddia eder ve bu diller genellikle nesne yönelimli nin ne anlama geldiği konusunda farklı bir yoruma ve kullandıkları kavramlar için farklı bir terminolojiye sahiptir.

O halde ayrıntılara çok fazla girmeden önce Ruby hakkında konuşmak için kullanacağımız terimlere ve gösterimlere kısaca göz atalım.

Nesneye yönelik programlar yazdığınızda, kavramları dış dünyadan veya mantıksal alanınızdan modellemeye çalışırsınız. Bu modelleme süreci sırasında, kodda temsil edilmesi gereken ilgili veri ve davranış kategorilerini keşfedeceksiniz. Müzik kutusunu temsil eden bir sistemde “şarkı” kavramı böyle bir kategori olabilir. Bir şarkı, durumu (örneğin şarkının adı) ve bu durumu (state) kullanan metotları (belki de şarkıyı çalmaya yönelik bir metodu) birleştirebilir. Ruby’de şarkıların genel durumunu temsil etmek için Song adında bir sınıf tanımlarsınız.

Bu sınıflara sahip olduğunuzda, genellikle her birinin birkaç ayrı örneğini oluşturmak isteyeceksiniz. Song adlı bir sınıf içeren müzik kutusu sistemi için, “Ruby Tuesday”, “Enveloped in Python”, “String of Pearls”, “Small Talk” vb. gibi farklı adlara sahip popüler hitler için ayrı örnekleriniz olacaktır. Bu örneklerin her birinin kendi durumu vardır ancak sınıfın ortak davranışını paylaşırlar. Nesne (object) kelimesi sıklıkla örnek (instance) kelimesiyle birbirinin yerine kullanılır.

Ruby’de örnekler, sınıfla ilişkili özel bir metot olan kurucunun (constructor) çağrılması yoluyla oluşturulur. Standart kurucuya new denir. new metodu sizin için Ruby tarafından tanımlanır ve bunu kendi başınıza tanımlamanıza gerek yoktur. Bunun gibi örnekler oluşturabilirsiniz:

song1 = Song.new("Ruby Tuesday")
song2 = Song.new("Enveloped in Python") 
# and so on

Bu örneklerin her ikisi de aynı sınıftan türetilmiştir ancak her birinin benzersiz özellikleri vardır. Her nesnenin, object_id özelliği aracılığıyla erişilebilen benzersiz bir nesne tanımlayıcısı vardır. Yukarıdaki örnekte, song1.object_id ve song2.object_id‘yi kontrol ederseniz, bunların farklı değerlere sahip olduğunu görürsünüz.

Her örnek için, o örneğe özgü değerlere sahip değişkenler tanımlayabilirsiniz. Bu örnek değişkenleri nesnenin durumunu tutar. Örneğin şarkılarımızın her biri, o şarkının adını taşıyan bir örnek değişkene sahip olacak.

Her sınıf içinde örnek metotları tanımlayabilirsiniz. Her metot, sınıf bağlamında ve genellikle sınıfın dışından çağrılabilen bir işlevsellik yığınıdır; ancak hangi metotların harici olarak kullanılabileceğine ilişkin kısıtlamalar belirleyebilirsiniz. Bu örnek metotların nesnenin örnek değişkenlerine ve dolayısıyla nesnenin durumuna erişimi vardır. Örneğin bir Song sınıfı, play adı verilen bir örnek metodu tanımlayabilir. Bir değişken belirli bir Song örneğine referans veriyorsa, o örneğin play metodunu çağırabilir ve o şarkıyı çalabilirsiniz.

Sözdizimsel olarak, bir metot nokta sözdizimi kullanılarak çağrılır; işte bazı örnekler:

# intro/puts_examples.rb
"gin joint".length # => 9
"Rick".index("c") # => 2
42.even? # => true
sam.play(song) # => "duh dum, da dum de dum ..."

Her satır çağrılan metodu gösterir. Noktadan önceki öğeye metodun alıcısı denir ve noktadan sonra gelen öğe çağrılacak metodun adıdır. İlk örnekte “gin joint” dizesinin uzunluğu sorulur; ikincisi farklı bir dizeden c harfinin indeksini bulmasını ister. Üçüncü satır 42 sayısının çift olup olmadığını sorar (soru işareti even? metot adının bir parçasıdır). Son olarak, sam adlı bir nesneden bize bir şarkı çalmasını istiyoruz (başka bir yerde tanımladığımız bir nesneye referans veren sam adında bir değişkenin olduğunu varsayarak).

Gönderilen metotlardan bahsettiğimizde genellikle nesneye bir mesaj gönderdiğimizi söyleriz. Mesaj, metodun adını ve metodun bekleyebileceği tüm argümanları içerir. Nesne, mesaja bu isimdeki metodu çağırarak yanıt verir. Metot çağrılarını nesnelere mesaj şeklinde ifade etme fikri Smalltalk programlama dilinden gelmektedir. Bir nesne bir mesaj aldığında, karşılık gelen bir metot için kendi sınıfını arar. Bulunursa bu metot yürütülür. Metot bulunamazsa, Ruby onu arar.

Burada Ruby ile diğer nesne yönelimli diller arasındaki büyük farka dikkat çekmekte yarar var. Örneğin Java’da, ayrı bir fonksiyonu çağırıp o sayıyı ileterek, sayının mutlak değerini bulursunuz.

num = Math.abs(num); // Java code

Ruby’de mutlak değer belirleme yeteneği, ayrıntılarla dahili olarak ilgilenen numbers sınıfına yerleştirilmiştir. Basitçe abs mesajını bir sayı nesnesine gönderirsiniz ve işi onun yapmasına izin verirsiniz:

num = -1234 # => -1234 
positive = num.abs # => 1234

Aynı durum tüm Ruby nesneleri için geçerlidir. Python’da len(name) yazarsınız, ancak Ruby’de bu name.length vb. şeklinde olur. Ruby’nin temel türleri olmayan saf nesne yönelimli bir dil olduğunu söylerken kastettiğimiz şey budur.

Temel Ruby

Pek çok insan yeni bir dil öğrenirken sıkıcı sözdizimi kurallarını okumaktan hoşlanmaz, bu yüzden hile yapacağız. Bu bölümde öne çıkanlara değineceğiz - Ruby programları yazacaksanız bilmeniz gereken şeyler var.

Basit bir Ruby programıyla başlayalım. Kişiselleştirilmiş selamlama döndüren bir metot yazacağız. Daha sonra bu metodu birkaç kez çağıracağız:

# intro/hello1.rb
def say_hello_goodbye(name)
  result = "I don't know why you say goodbye, " + name + ", I say hello"
  return result
end
# call the method
puts say_hello_goodbye("John")
puts say_hello_goodbye("Paul")
# I don't know why you say goodbye, John, I say hello
# I don't know why you say goodbye, Paul, I say hello

Örnekte görüldüğü gibi Ruby’nin sözdizimi düzenlidir. Her bir ifadeyi ayrı bir satıra koyduğunuz sürece ifadelerin sonunda noktalı virgül kullanmanıza gerek yoktur. Ruby yorumları # karakteriyle başlar ve satırın sonuna kadar devam eder. Kod düzeni size kalmış; girinti önemli değildir. Bununla birlikte, iki karakterli girinti Ruby topluluğunun en çok tercih ettiği seçenektir.

Metotlar, def anahtar sözcüğü, ardından metot adı (bu durumda ad say_hello_goodbye) ve ardından parantez içindeki metot parametreleri ile tanımlanır (Aslında parantezler isteğe bağlıdır, ancak bunları kullanmanızı öneririz). Ruby, bileşik ifadelerin ve tanımların gövdelerini sınırlamak için parantez kullanmaz. Bunun yerine, gövdeyi end anahtar sözcüğüyle tamamlamanız yeterlidir. Metodun gövdesi oldukça basittir. İlk satır, “I don’t know why you say goodbye, “ dizesini ve parametre adı ile “, I say hello” dizesini birleştirir ve sonucu local değişken result‘a atar. Bir sonraki satırda bu sonucu döndürür. result değişkenini bildirmemize gerek olmadığını unutmayın; biz ona değer atadığımızda ortaya çıktı.

Metodu tanımladıktan sonra onu iki kez çağırıyoruz. Her iki durumda da sonucu puts metoduna aktarıyoruz, bu metot basitçe argümanı ve ardından yeni satır verir (sonraki çıktı satırına geçer):

puts say_hello_goodbye("John")
puts(say_hello_goodbye("John"))

Hayat her zaman basit değildir ve öncelik kuralları, hangi argümanın hangi metot çağrısına uygun olduğunu bilmeyi zorlaştırabilir; bu nedenle en basit durumlar dışında tüm durumlarda parantez kullanmanızı öneririz. Metodun açık bir alıcısı olmadığı ve yalnızca bir argümanı olduğu zaman Ruby programlarının sıklıkla parantezleri atladığını göreceksiniz.

Bu örnek aynı zamanda Ruby string nesnelerini de göstermektedir. String nesnesi oluşturmanın birçok yolu vardır, ancak en yaygın olanı, tek veya çift tırnak işaretleri arasındaki karakter dizileri kullanmaktır. İki form, Ruby’nin literal oluştururken string üzerinde yaptığı işlem miktarı açısından farklılık gösterir. Tek tırnaklı durumda Ruby çok az şey yapar. Birkaç istisna dışında, string literal’e girdiğiniz şey string’in değeri olur.

Çift tırnaklı durumda Ruby daha fazla iş yapar. Öncelikle ters eğik çizgi karakteriyle başlayan ikame dizilerini arar ve bunları bir binary değerle değiştirir. Bu ikamelerden en yaygın olanı, yeni satır karakteriyle değiştirilen \n‘dir. Yeni satır içeren bir string çıktısı alındığında, bu yeni satır, satır sonu haline gelir:

puts "Hello and goodbye to you,\nGeorge"

# Hello and goodbye to you,
# George

Ruby’nin çift tırnaklı dizelerle yaptığı ikinci şey ifade enterpolasyonudur. Dize içinde #{EXPRESSION} dizisinin yerini EXPRESSION değeri alır. Bunu önceki metodumuzu yeniden yazmak için kullanabiliriz:

def say_hello_goodbye(name)
  result = "I don't know why you say goodbye, #{name}, I say hello"
  return result
end
puts say_hello_goodbye("Ringo")
# I don't know why you say goodbye, Ringo, I say hello

Ruby bu string nesnesini oluşturduğunda, name‘in mevcut değerine bakar ve onu string’in yerine koyar. #{...} yapısında karmaşık ifadelere izin verilir. Aşağıdaki örnekte, parametremizi büyük harfle başlatmak için capitalize metodunu çağırıyoruz:

def say_hello_goodbye(name)
  result = "I don't know why you say goodbye, #{name.capitalize}, I say hello"
  return result
end
puts say_hello_goodbye("john")
# I don't know why you say goodbye, John, I say hello

say_hello_goodbye metodumuzu biraz daha basitleştirebiliriz. Açık bir return ifadesinin yokluğunda, Ruby metodunun döndürdüğü değer, değerlendirilen son ifadenin değeridir, böylece geçici değişkenden ve return ifadesinden tamamen kurtulabiliriz.

def say_hello_goodbye(name)
  "I don't know why you say goodbye, #{name}, I say hello"
end
puts say_hello_goodbye("Paul")
# I don't know why you say goodbye, Paul, I say hello

Bu sürüm daha deyimsel (idiomatic) olarak kabul edilir; bununla, uzman Ruby programcılarının Ruby programlarını yazmayı seçme şekliyle daha uyumlu olduğunu kastediyoruz. Idiomatic Ruby, Ruby’nin kısayollarına ve belirli sözdizimine güvenme eğilimindedir. Idiomatic Ruby stiline ilişkin yönergeleri https://github.com/testdouble/standard adresinde bulabilirsiniz, ki bu kitaptaki kodlar için kullanılmıştır (bir noktaya değinmek için kurallarını kasıtlı olarak ihlal ettiğimiz durumlar hariç).

Bu bölümün kısa olacağına söz vermiştik. Ele almamız gereken bir konu daha var: Ruby adları. Kısaca açıklamak gerekirse, burada tanımlamayacağımız bazı terimleri (sınıf değişkeni gibi) kullanacağız. Ancak, şimdi kurallar hakkında konuşarak, sınıf değişkenlerini ve benzerlerini daha sonra tartışmaya başladığımızda oyunun ilerisinde olacaksınız.

Ruby ilk başta tuhaf görünebilecek bir kural kullanıyor: Bir ismin ilk karakterleri değişkenin ne kadar görünür olduğunu gösterir. Local değişkenler, metot parametreleri ve metot adlarının tümü küçük harfle veya alt çizgiyle başlamalıdır (Ruby’nin kendisinde büyük harfle başlayan birkaç metot vardır, ancak genel olarak bu kendi kodunuzda yapılacak bir şey değildir).

Global değişkenlere dolar işareti $ eklenir ve örnek değişkenler “et” işareti @ ile başlar. Sınıf değişkenleri iki “et” @@ işaretiyle başlar. Her ne kadar burada konuyu tamamlamak adına global ve sınıf değişkenlerinden bahsetsek de, bunların Ruby programlarında nadiren kullanıldığını göreceksiniz. Global değişkenlerin programların sürdürülmesini zorlaştırdığına dair pek çok kanıt var. Sınıf değişkenleri global değişkenler kadar tehlikeli değildir, ancak güvenli bir şekilde kullanılması hala zordur; sadece insanlar bunları pek kullanmama eğilimindedir çünkü benzer işlevleri elde etmenin genellikle daha kolay yolları vardır. Son olarak sınıf adları, modül adları ve diğer sabitler büyük harfle başlamalıdır.

Bu başlangıç karakterinden sonra ad, herhangi bir harf, rakam ve alt çizgi kombinasyonunu içerebilir, istisna olarak @ işaretini takip eden karakter rakam olamaz. Bununla birlikte, geleneksel olarak, çok kelimeli örnek değişkenler, first_name veya zip_code gibi kelimelerin arasına alt çizgilerle yazılır ve çok kelimeli sınıf adları, FirstName veya ZipCode gibi her kelime büyük harfle yazılacak şekilde MixedCase (bazen CamelCase olarak da adlandırılır) yazılır. Sabitler, FIRST_NAME veya ZIP_CODE gibi sözcükler alt çizgilerle ayrılmış şekilde tamamen büyük harflerle yazılır. Metot adları ?, ! ve = karakterleriyle bitebilir.

Diziler ve Hash’ler

Ruby, nesneleri koleksiyonlarda birleştirmenin birkaç farklı yolunu sunar. Çoğu zaman bunlardan ikisini kullanacaksınız: Diziler ve Hash’ler. Dizi, nesnelerin doğrusal bir listesidir; bunları, ilk slot için sıfırdan başlayarak dizideki yerlerinin sayısı olan indeksleri aracılığıyla alırsınız. Hash bir ilişkilendirmedir, yani her değerin isteğe bağlı bir anahtara sahip olduğu anahtar/değer deposudur ve değeri bu anahtar aracılığıyla alırsınız. Hem diziler hem de hash’ler, yeni öğeleri tutacak şekilde gerektiği kadar büyür. Herhangi bir dizi veya hash, farklı türdeki nesneleri tutabilir; Birazdan göreceğimiz gibi, önce tamsayı, sonra dize, sonra da kayan noktalı sayı içeren bir diziniz olabilir.

Bir diziyi (köşeli parantezler arasındaki öğeler kümesi) kullanarak yeni bir dizi nesnesi oluşturabilir ve başlatabilirsiniz. Bir dizi nesnesi verildiğinde, sonraki örnekte gösterildiği gibi, indeks ile tek tek öğelere erişebilirsiniz. Ruby dizi indekslerinin sıfırdan başladığını unutmayın.

a = [1, 'cat', 3.14] # array with three elements 
puts "The first element is #{a[0]}"
# set the third element
a[2] = nil
puts "The array is now #{a.inspect}"

# The first element is 1
# The array is now [1, "cat", nil]

Bu örnekte nil özel değerini kullandığımızı fark etmiş olabilirsiniz. Pek çok dilde nil (veya null) kavramı “nesne yok” anlamına gelir. Ruby’de durum böyle değildir; nil, tıpkı diğerleri gibi bir nesnedir. Bu sadece hiçlik kavramını temsil eden bir nesne. Neyse, dizilere ve hash’lere geri dönelim.

Ruby hash sözdizimi dizi sözdizimine benzer. Hash köşeli parantez yerine süslü parantez kullanır. Literal değer her giriş için iki nesne sağlamalıdır: biri anahtar, diğeri değer için. Genel olarak, anahtar ve değer => ile ayrılır, ancak çok yaygın olarak kullanılan bir kısayolun da olduğunu göreceğiz.

Örneğin, müzik enstrümanlarını orkestral bölümlerle eşlemek için hash kullanabilirsiniz.

instrument_section = {
  'cello' => 'string',
  'clarinet' => 'woodwind',
  'drum' => 'percussion',
  'oboe' => 'woodwind',
  'trumpet' => 'brass',
  'violin' => 'string'
}

=> işaretinin solundaki şey anahtardır, sağındaki şey ise karşılık gelen değerdir. Belirli bir hash’teki anahtarlar benzersiz olmalıdır; “drum” için iki girişiniz olamaz. Hash’teki anahtarlar ve değerler isteğe bağlı nesneler olabilir. Değerlerin diziler, diğer hash’ler vb. olduğu hash’lere sahip olabilirsiniz. Hash’teki anahtarların sırası sabittir ve her zaman anahtarların eklendiği sırayla eşleşecektir. Bir anahtara yeni değer atarsanız eski değer silinir.

Hash’ler dizilerle aynı köşeli parantez gösterimini kullanılarak indekslenir.

p instrument_section["oboe"] # => "woodwind"\n"string"
p instrument_section["cello"] # => "string"\nnil
p instrument_section["bassoon"] # => nil

Hash’in içermediği bir anahtarı kullandığınızda varsayılan davranışı, değerin yokluğunu temsil eden nil değerini döndürmektir.

Bazen bu varsayılan davranışı değiştirmek isteyebilirsiniz. Örneğin, bir dosyada her kelimenin kaç kez geçtiğini saymak için hash kullanıyorsanız, varsayılan değerin sıfır olması uygun olur. Daha sonra kelimeyi anahtar olarak kullanabilir ve o kelimeyi daha önce görüp görmediğiniz konusunda endişelenmeden karşılık gelen hash değerini artırabilirsiniz. Bu, yeni, boş bir hash oluşturduğunuzda varsayılan değer belirtilerek yapılabilir:

histogram = Hash.new(0) # The default value is zero 
histogram["ruby"] # => 0
histogram["ruby"] = histogram["ruby"] + 1 
histogram["ruby"] # => 1

Semboller

Çoğu zaman programlama sırasında aynı dizeyi tekrar tekrar kullanmanız gerekir. Belki de dize hash’teki bir anahtardır veya belki de dize bir metodun adıdır. Bu durumda, muhtemelen dizeye erişimin sabit olmasını, böylece değerinin değişmemesini istersiniz ve ayrıca dizeye erişimin olabildiğince hızlı olmasını ve mümkün olduğunca az bellek kullanmasını istersiniz.

Bu da bizi Ruby’nin sembollerine getiriyor. Semboller tam olarak optimize edilmiş dizeler değildir, ancak çoğu amaç için bunları sabit olan, yalnızca bir kez oluşturulan ve aramanın çok hızlı olduğu özel dizeler olarak düşünebilirsiniz. Sembollerin anahtarlar ve tanımlayıcılar olarak kullanılması amaçlanırken, dizelerin veriler için kullanılması amaçlanmaktadır.

Bir sembol iki nokta üst üste ile başlar ve ardından ad gelir:

walk(:north)
look(:east)

Bu örnekte, koddaki sabit değerleri temsil etmek için :north ve :east sembollerini kullanıyoruz. Sembolleri bildirmemize veya onlara bir değer atamamıza gerek yok; Ruby bunu sizin için halleder. Bir sembolün değeri ismine eşdeğerdir.

Ruby ayrıca programınızda nerede görünürlerse görünsün, aynı ada sahip sembollerin aynı değere sahip olacağını, aslında aynı nesne olacağını garanti eder. Sonuç olarak, aşağıdakileri güvenle yazabilirsiniz:

def walk(direction)
  nil unless direction == :north
  # ...
end

Değerleri değişmediği için semboller sıklıkla hash’lerde anahtar olarak kullanılır. Önceki hash örneğimizi sembolleri anahtar olarak kullanarak yazabiliriz:

instrument_section = { 
  :cello => "string", 
  :clarinet => "woodwind", 
  :drum => "percussion", 
  :oboe => "woodwind", 
  :trumpet => "brass", 
  :violin => "string"
}

instrument_section[:oboe] # => "woodwind"
instrument_section[:cello] # => "string"
# Note that strings aren"t the same as symbols...
instrument_section["cello"] # => nil

Son satırda, sembol anahtarının dize anahtarından farklı olduğunu ve bir anahtar üzerinden erişimin diğeriyle ilişkili bir değerle sonuçlanmayacağını unutmayın.

Semboller hash anahtarları olarak o kadar sık kullanılır ki Ruby’nin bunun için kısayol söz dizimi vardır: anahtar bir sembol ise name => value yerine hash oluşturmak için name: value çiftlerini kullanabilirsiniz:

instrument_section = {
  cello: "string",
  clarinet: "woodwind",
  drum: "percussion",
  oboe: "woodwind",
  trumpet: "brass",
  violin: "string"
}

puts "An oboe is a #{instrument_section[:oboe]} instrument"
# An oboe is a woodwind instrument

Bu sözdizimi kısmen, her ikisi de anahtar/değer çiftlerinde ayırıcı olarak iki nokta üst üste kullanan JavaScript ve Python’a aşina olan programcılar için eklenmiştir.

Kontrol Yapıları

Ruby, if ifadeleri ve while döngüleri gibi tüm olağan kontrol yapılarına sahiptir. Java veya JavaScript programcıları bu ifadelerin gövdelerinin etrafındaki parantezlerin olmayışı karşısında şaşırabilirler. Bunun yerine Ruby, kontrol yapısının gövdesinin sonunu belirtmek için end anahtar sözcüğünü kullanır:

today = Time.now

if today.saturday?
  puts "Do chores around the house"
elsif today.sunday?
  puts "Relax"
else
  puts "Go to work"
end

# Relax

Alışılmadık bulabileceğiniz bir şey, Ruby’nin ikinci cümlede “else if”i belirtmek için elsif anahtar kelimesini (bir “e” eksik, tek kelime) kullanmasıdır. Bu anahtar kelimeyi else if şeklinde kullanmak sözdizimi hatasına neden olur.

Benzer şekilde, while ifadeleri, while satırındaki koşul doğru olduğu sürece end ile sonlandırılır:

num_pallets = 0
weight = 0

while weight < 100 && num_pallets <= 5
  pallet = next_pallet()
  weight += pallet.weight
  num_pallets += 1
end

Ruby’deki ifadelerin çoğu değer döndürür; bu da ifadeleri koşul olarak kullanabileceğiniz anlamına gelir. Örneğin, gets metodu, bir sonraki satırı veya dosyanın sonuna ulaşıldığında nil değerini döndürür. Ruby, koşullarda nil değerini false olarak ele aldığından, bir dosyadaki satırları işlemek için aşağıdakileri yazabilirsiniz:

while (line = gets)
  puts line.downcase
end

Atama ifadesi line değişkenini, metnin bir sonraki satırı veya nil olacak olan gets çağrısının sonucuna ayarlar. Daha sonra while ifadesi, atama ifadesinin döndürdüğü değeri, yani atanan değeri test eder. Değer nil olduğunda bu, çıktıda başka satır olmadığı ve while döngüsünün sona erdiği anlamına gelir. Ruby ifade değiştiricileri (statement modifiers), if veya while ifadesinin gövdesi yalnızca tek bir ifadeden oluşuyorsa kullanışlı bir kısayoldur. Basitçe ifadeyi yazın, ardından if, while ve koşulu yazmanız yeterlidir. Örneğin:

if radiation > 3000
  puts "Danger, Will Robinson"
end

İfade değiştirici kullanılarak yeniden yazıldığında:

puts "Danger, Will Robinson" if radiation > 3000

Benzer şekilde bu while döngüsü:

square = 4
while square < 1000
  square = square * square
end

şu daha kısa versiyona dönüşür:

square = 4
square = square * square while square < 1000

Bu değiştiricilerin if sürümü belki de en yaygın olarak, return nil if user.nil? örneğinde olduğu gibi, bir metodun başlangıcında koruma ifadesi olarak kullanılır. while versiyonu çok daha az kullanılır.

Düzenli İfadeler (Regular Expressions)

Ruby’nin yerleşik türlerinin çoğu tüm programcılara tanıdık gelecektir. Dillerin çoğunda dizeler, tamsayılar, kayan değerler, diziler vb. bulunur. Ancak tüm diller, Ruby veya JavaScript’in aksine, düzenli ifadeler için yerleşik desteğe sahip değildir. Bu üzücü, çünkü düzenli ifadeler, şifreli olmasına rağmen metinle çalışmak için güçlü bir araçtır. Ve bunları bir kütüphane arayüzü aracılığıyla oluşturmak yerine yerleşik hale getirmek büyük bir fark yaratıyor.

Düzenli ifadeler hakkında kitaplar yazılmıştır (örneğin Jeffrey Friedl’in Mastering Regular Expressions adlı kitabı), bu nedenle bu kısa bölümde her şeyi kapsamaya çalışmayacağız. Bunun yerine, düzenli ifadelerin yalnızca birkaç örneğine bakacağız. Düzenli ifadelerle ilgili daha fazla bilgiyi ilerleyen bölümlerde bulacaksınız.

Düzenli ifade, bir dizede eşleştirilecek karakterlerin patern’ini belirtmenin bir yoludur. Ruby’de genellikle eğik çizgi karakterleri (/pattern/) arasına patern yazarak düzenli ifade oluşturursunuz. Ve Ruby, Ruby olduğundan, düzenli ifadeler nesnelerdir ve bu şekilde değiştirilebilirler.

Örneğin, aşağıdaki düzenli ifadeyi kullanarak Ruby metnini veya Rust metnini içeren dizeyle eşleşen bir patern yazabilirsiniz:

/Ruby|Rust/

Eğik çizgiler, eşleştirdiğimiz iki şeyden oluşan patern’i dikey çizgi karakteriyle (|) ayırır. Düzenli ifadelerde dikey çizgi karakteri “ya sağdaki şey ya da soldaki şey” anlamına gelir, bu durumda ya Ruby ya da Rust. Tıpkı aritmetik ifadelerde olduğu gibi patern’lerin içinde parantezleri kullanabilirsiniz, böylece bu patern aynı dize kümesiyle eşleşir:

/Ru(by|st)/

Ayrıca patern içindeki tekrarı da belirleyebilirsiniz. /ab+c/, bir a ve ardından bir veya daha fazla b ve ardından bir c içeren dizeyle eşleşir. Artıyı yıldız işaretiyle değiştirdiğinizde /ab*c/, bir a, sıfır veya daha fazla b ve bir c ile eşleşen düzenli ifade oluşturur.

Ayrıca patern içindeki bir grup karakterden birini de eşleştirebilirsiniz. Bazı yaygın örnekler, boşluk karakteriyle (boşluk, sekme, yeni satır vb.) eşleşen \s gibi karakter sınıflarıdır; Herhangi bir rakamla eşleşen \d; ve \w, tipik bir sözcükte bulunabilecek herhangi bir karakterle eşleşir. Nokta (.), (neredeyse) her karakterle eşleşir.

Bazı faydalı düzenli ifadeler üretmek için tüm bunları bir araya getirebiliriz:

/\d\d:\d\d:\d\d/ # a time such as 12:34:56
/Ruby.*Rust/ # Ruby, zero or more other chars, then Rust
/Ruby Rust/ # Ruby, exactly one space, and Rust
/Ruby *Rust/ # Ruby, zero or more spaces, and Rust
/Ruby +Rust/ # Ruby, one or more spaces, and Rust
/Ruby\s+Rust/ # Ruby, one or more whitespace characters, then Rust
/Java (Ruby|Rust)/ # Java, a space, and either Ruby or Rust

Match operatörü =~, bir dizeyi düzenli ifade ile eşleştirmek için kullanılabilir. Patern dizede bulunursa, başlangıç konumunu döndürür; aksi takdirde nil değerini döndürür. Bu, if ve while ifadelerinde koşul olarak düzenli ifadeleri kullanabileceğiniz anlamına gelir. Örneğin, aşağıdaki kod parçası, bir dizenin Ruby veya Rust metnini içermesi durumunda ekrana bir mesaj yazar:

line = gets
if line =~ /Ruby|Rust/
  puts "Programming language mentioned: #{line}"
end

Hem dizeler hem de düzenli ifadeler, =~ operatörüyle eşanlamlı olan match? metoduna sahiptir:

line = gets
if line.match?(/Ruby|Rust/)
  puts "Scripting language mentioned: #{line}"
end

match? formu muhtemelen Ruby’de daha yaygın kullanılır.

Bir dizenin düzenli ifade ile eşleşen kısmı, Ruby’nin değiştirme metotlarından biri kullanılarak farklı bir metinle değiştirilebilir:

line = gets
newline = line.sub(/Python/, 'Ruby') # replace first 'Python' with 'Ruby'
newerline = line.gsub(/Python/, 'Ruby') # replace every 'Python' with 'Ruby'

Bunu kullanarak her JavaScript ve Python kelimesini Ruby ile değiştirebilirsiniz:

line = gets
newline = line.gsub(/JavaScript|Python/, 'Ruby')

Kitabı okudukça düzenli ifadeler hakkında daha çok şey öğreneceksiniz.

Bloklar

Bu bölümde Ruby’nin güçlü yönlerinden biri olan bloklar kısaca anlatılmaktadır. Kod bloğu, sanki blok başka bir parametreymiş gibi, bir metoda iletebileceğiniz kod yığınıdır. Bu, Ruby metotlarının son derece esnek olmasını sağlayan inanılmaz derecede güçlü bir özelliktir. Bu kitabı ilk inceleyenlerden biri bu noktada şu yorumu yaptı: “Bu oldukça ilginç ve önemli, dolayısıyla daha önce dikkat etmediyseniz muhtemelen şimdi başlamalısınız.” Hala aynı fikirdeyiz.

Sözdizimsel olarak kod blokları iki şekilde sınırlandırılabilen kod parçalarıdır: parantez arasında veya do ile end arasında. Bu, bir mesaj çağrısının sonundaki kod bloğudur:

foo.each { puts "Hello" }

Bu da aynı zamanda bir mesaj çağrısının sonundaki kod bloğudur:

foo.each do
  club.enroll(person)
  person.socialize
end

İki tür blok sınırlayıcının önceliği farklıdır: parantezler do/end çiftlerinden daha sıkı bağlanır, bu da kodunuzda neredeyse hiçbir zaman fark yaratmayacak bir gerçektir. Pratikte en sık göreceğiniz standart, tek satırlı bloklar için parantez, çok satırlı bloklar için do/end kullanmaktır.

Metot blokla hiçbir şey yapmasa bile, bir bloğu herhangi bir metot çağrısına argüman olarak iletebilirsiniz. Bunu, bloğu metot çağrısının sonunda, diğer parametrelerden sonra başlatarak yaparsınız. Örneğin, aşağıdaki kodda, puts "Hi" içeren blok, greet metoduna yapılan çağrıyla ilişkilidir (bunu burada göstermiyoruz):

greet { puts "Hi" }

Metodun parametreleri varsa, bunlar bloktan önce görünür ve metot çağrısı başına yalnızca bir blok verebilirsiniz.

verbose_greet("Dave", "loyal customer") { puts "Hi" }

Daha sonra bir metot, Ruby yield ifadesini kullanarak ilişkili bloğu bir veya daha fazla kez çağırabilir. yield ifadesi, metodu iletilen bloğu çağırarak kontrolü bloğun içindeki koda aktarır.

Aşağıdaki örnekte blok çağrısının çalışması gösterilmektedir. İki kez yield çağıran bir metot tanımlıyoruz. Daha sonra bu metodu çağırıp, herhangi bir argümandan sonra aynı satıra bir blok yerleştirin. Bloğun metotla ilişkilendirilmesini bir tür argüman aktarımı olarak düşünebilirsiniz. Aslında hikayenin tamamı bu değil. Blok, başka bir metoda argüman olarak çağrılabilen veya iletilebilen, etkili bir şekilde tamamen başka bir metottur. Örneğin:

def call_block
  puts "Start of method"
  yield
  yield
  puts "End of method"
end

call_block { puts "In the block" }

# Start of method
# In the block
# In the block
# End of method

Bu örnekte, bloktaki kod (puts "In the block"), kontrolü bloğa aktaran yield çağrısı için bir kez olmak üzere iki kez yürütülür.

yield için argümanlar sağlayabilirsiniz ve bunlar bloğa iletilecektir. Blok içinde, bu argümanları almak için parametrelerin adlarını dikey çubuklar (|params...|) arasında listeleyin. Aşağıdaki örnek, ilişkili bloğu iki kez çağıran ve her seferinde bloğa iki argüman ileten bir metodu gösterir:

def who_says_what
  yield("Dave", "hello")
  yield("Andy", "goodbye")
end
who_says_what { |person, phrase| puts "#{person} says #{phrase}" }

# Dave says hello
# Andy says goodbye

Daha sonra callback uygulamak amacıyla kodu paketlemek için kod bloklarını kullanabilirsiniz. Kod blokları kod parçalarını aktarmak için kullanılabilir ve Ruby standart kütüphanesi boyunca, metotların dizi gibi bir koleksiyondaki ardışık öğeler üzerinde eylem gerçekleştirmesine izin vermek için kullanılır. Koleksiyondaki tüm nesnelere benzer bir şey yapma eylemine Ruby’de enumeration denir; diğer diller buna iteration adını verir.

animals = ["ant", "bee", "cat", "dog"] # create an array
animals.each { |animal| puts animal } # iterate over the contents

# ant
# bee
# cat
# dog

Java ve JavaScript gibi dillerde yerleşik olan döngü yapılarının çoğu, Ruby’deki metot çağrılarıdır ve metotlar ilişkili bir bloğu sıfır veya daha fazla kez çağırır:

["cat", "dog", "horse"].each { |name| print name, " " }
5.times { print "*" }
3.upto(6) { |i| print i }
("a".."e").each { |char| print char }
("a".."e").each { print _1 }

# cat dog horse *****3456abcdeabcde

İlk satırda bir diziden, bloğun her elemanı için bir kez çağrı yapmasını istiyoruz. Daha sonra nesne 5, her seferinde * yazdırarak bloğu beş kez çağırır. Üçüncü örnek, for döngülerini kullanmak yerine, Ruby’de 3 rakamından bir blok çağırmasını isteyebileceğimizi ve 6’ya ulaşana kadar ardışık değerler iletebileceğimizi gösterir. Son olarak, değer aralıkları için Ruby’nin literal sözdizimini kullanarak a’dan e’ye kadar olan karakter aralığının her biri için bloğu çağırmasını sağlıyoruz: bu örneği iki kez gösteriyoruz; bir kez Ruby’nin normal blok parametresi sözdizimini kullanarak ve ikinci olarak blok parametreleri için Ruby’nin kısayolunu kullanarak.

Okuma ve Yazma

Ruby, input-output (I/O) işlemlerini yönetmek için kapsamlı bir kütüphane ile birlikte gelir. Ancak bu kitaptaki örneklerin çoğunda birkaç basit metoda sadık kalacağız. Çıktı yazan metotlarla zaten karşılaştık: puts argümanlarını her birinin arkasına yeni satır ekleyerek yazar; p de argümanlarını da yazar ancak daha fazla hata ayıklanabilir çıktı üretecektir. Her ikisi de herhangi bir I/O nesnesine yazmak için kullanılabilir, ancak varsayılan olarak standart ouput’a yazarlar.

Programınızda input’u okumanın birçok yolu vardır. Muhtemelen en geleneksel olanı, standart input’tan bir sonraki satırı döndüren gets (“get string”in kısaltmasıdır) metodunu kullanmaktır:

line = gets
print line

gets input’un sonuna ulaştığında nil değerini döndürdüğü için, dönüş değerini döngü koşulunda kullanabilirsiniz. Aşağıdaki kodda while koşulunun bir değer ataması olduğuna dikkat edin:

while (line = gets)
  print line
end

İlerleyen bölümlerde bir dosyadan veya başka bir veri kaynağından nasıl okunup yazılacağı hakkında daha fazla konuşacağız.

Komut Satırı Argümanları

Bir Ruby programını komut satırından çalıştırdığınızda argümanları iletebilirsiniz. Bunlara Ruby kodunuzdan iki farklı şekilde erişilebilir.

İlk olarak, global ARGV dizisi, çalışan programa aktarılan argümanların her birini içerir. Aşağıdaki kodu cmd_line.rb adında bir dosyaya yerleştirin:

puts "You gave #{ARGV.size} arguments"
p ARGV

Argümanlarla çalıştırdığımızda, bunların aktarıldığını görebiliriz:

$ ruby cmd_line.rb ant bee cat dog
You gave 4 arguments
["ant", "bee", "cat", "dog"]

Genellikle bir programın argümanları, işlemek istediğiniz dosyaların adlarıdır. Bu durumda ikinci bir teknik kullanabilirsiniz: ARGF değişkeni özel bir tür I/O nesnesidir, adları komut satırında iletilen tüm dosyaların içerikleri gibi davranır (veya herhangi bir dosya adını iletmezseniz standart input). Bunu ilerleyen bölümlerde daha detaylı inceleyeceğiz.

Ruby’de Yorumlar

Ruby koduna yorum eklemenin iki yolu vardır; bunlardan birini kullanacaksınız ve birini neredeyse hiç kullanmayacaksınız. Yaygın olanı # sembolüdür; bu sembolden sonra satırın sonuna kadar olan her şey bir yorumdur ve yorumlayıcı tarafından dikkate alınmaz. Bir sonraki satır yoruma devam ediyorsa kendi # sembolüne ihtiyacı vardır.

Ruby ayrıca nadiren kullanılan çok satırlı bir yoruma da sahiptir; burada ilk satır =begin ile başlar ve kod =end‘e ulaşana kadar her şey bir yorumdur. Hem =begin hem de =end satırın en başında olmalıdır; girintili olamazlar.

Az önce Ruby’nin yorumları göz ardı ettiğini söylemiş olsak da Ruby, dosya bazında yapılandırma seçenekleri için çok az sayıda “sihirli yorum” kullanır. Bu yorumlar # directive: value biçimindedir ve dosyada asıl Ruby kodunun ilk satırından önce görünmelidir.

En sık kullanılan sihirli yorum # frozen_string_literal: true‘dur. Bu yönerge true ise, içinde enterpolasyon bulunmayan her dize literali, sanki üzerinde frozen çağrılmış gibi otomatik olarak dondurulacaktır.

Ayrıca, belirli bir dosyanın içindeki dize ve düzenli ifade literal lerinin kodlamasını belirten # encoding: VALUE yönergesini de görebilirsiniz. Ruby bir dosyanın girintisinin eşleşmemesi durumunda kod uyarıları verecek # warn_indent: BOOLEAN flag’ine sahiptir. # sharable_constant_value adlı deneysel bir yönerge de vardır: Ractor multithreading aracı kullanılarak değerlerin nasıl paylaşıldığını etkiliyor.

Sıradaki

Ruby’nin bazı temel özelliklerine ışık hızında turumuzu tamamladık. Nesnelere, metotlara, dizelere, container ve düzenli ifadelere göz attık; basit kontrol yapılarını gördük; ve oldukça şık iteratör’lere baktık. Bu bölümün size bu kitabın geri kalanına saldırmanıza yetecek kadar mühimmat verdiğini umuyoruz.

Artık ilerlemenin ve daha yüksek bir seviyeye çıkmanın zamanı geldi. Daha sonra, Ruby’deki en üst düzey yapılar hem de tüm dilin temeli olan sınıflara ve nesnelere bakacağız.

Kaynak: “Programming Ruby 3.2: The Pragmatic Programmers’ Guide”, Noel Rappin with Dave Thomas