Ruby'de hem proc hem lambda Proc sınıfının nesneleridir; aralarındaki fark davranıştadır. Lambda argüman sayısını metot gibi katı biçimde kontrol eder ve içindeki return yalnızca lambdadan çıkar. Normal proc ise eksik argümanları nil ile doldurur, fazlalarını atar, tek dizi argümanını parçalarına ayırır ve içindeki return onu çevreleyen metottan çıkar. Hangisi olduğunu lambda? metoduyla öğrenebilirsiniz.

Bu yazıda proc ve lambda oluşturmanın yollarını, iki türün argüman ve return farklarını, arity, curry, fonksiyon birleştirme ve &:sembol kısayolunun arkasındaki to_proc protokolünü anlatıyoruz. Örnekler Ruby 4.0 ile çalıştırıldı. Blokların temeli için önce blok ve yield yazısını okuyabilirsiniz.

Proc oluşturmanın yolları

Proc oluşturmanın yolları

Belgeler beş yol sayar: Proc.new, onun kısaltması olan Kernel#proc, bir metodun &block parametresiyle bloğu yakalamak, Kernel#lambda ve ->(x) { } biçimindeki lambda literal yazımı. İlk üçü normal proc, son ikisi lambda üretir. Bir proc'u çağırmak için call, .(), [] ya da yield kullanabilirsiniz; hepsi aynı işi yapar.

ruby
kare1 = Proc.new { |x| x * x }
kare2 = proc { |x| x * x }
kare3 = lambda { |x| x * x }
kare4 = ->(x) { x * x }

p kare1.call(3) # çıktı: 9
p kare2.(3)     # çıktı: 9
p kare3[3]      # çıktı: 9
p kare4.yield(3) # çıktı: 9
p [kare1, kare2, kare3, kare4].map(&:lambda?) # çıktı: [false, false, true, true]
p kare4.class   # çıktı: Proc

Argüman kontrolü: esnek proc, katı lambda

Lambdalarda argümanlar metotlardaki gibi işlenir: sayı tutmazsa ArgumentError oluşur ve ek bir dönüştürme yapılmaz. Normal proc'lar daha hoşgörülüdür. Belgelere göre eksik argümanlar nil olur, fazla argümanlar sessizce atılır ve birden fazla parametreli bir proc'a tek bir dizi verilirse dizi parçalarına ayrılır.

ruby
pr = proc { |a, b| [a, b] }
la = lambda { |a, b| [a, b] }

p pr.call(1)       # çıktı: [1, nil]
p pr.call(1, 2, 3) # çıktı: [1, 2]
p pr.call([1, 2])  # çıktı: [1, 2]
p la.call(1, 2)    # çıktı: [1, 2]

[[1], [1, 2, 3], [[1, 2]]].each do |args|
  la.call(*args)
rescue ArgumentError => e
  puts "#{e.class}: #{e.message}"
end
# çıktı: ArgumentError: wrong number of arguments (given 1, expected 2)
# çıktı: ArgumentError: wrong number of arguments (given 3, expected 2)
# çıktı: ArgumentError: wrong number of arguments (given 1, expected 2)

Bu hoşgörü, belgelerin dediği gibi proc'ları iteratörler için kullanışlı yapar. map gibi metotlara verdiğiniz bloklar normal proc gibi davrandığı için [[1, 2], [3, 4]].map { |a, b| a + b } yazdığınızda iç diziler kendiliğinden a ve b'ye ayrılır. Aynı işi lambda ile yapmaya kalkarsanız map(&lambda) çağrısı ArgumentError verir, çünkü lambda niteliği & ile bloğa çevrildiğinde de korunur.

return ve break davranışı

En önemli fark burada. Lambda içindeki return ve break yalnızca lambdadan çıkar, tıpkı bir metottan dönmek gibi. Normal proc içindeki return ise proc'un tanımlandığı metottan çıkar. Belgelerdeki örneğin uyarlaması aşağıda: lambdanın return 3 satırı metodu durdurmaz, ama proc'un return 4 satırı metodu hemen bitirir.

ruby
def deneme
  -> { return 3 }.call
  proc { return 4 }.call
  return 5
end
p deneme # çıktı: 4

def uret
  proc { return 1 }
end
begin
  uret.call
rescue LocalJumpError => e
  puts "#{e.class}: #{e.message}" # çıktı: LocalJumpError: unexpected return
end

İkinci örnekteki duruma belgeler "orphaned Proc" (sahipsiz proc) der: proc, onu oluşturan metot bittikten sonra hâlâ yaşıyorsa return çıkacak bir metot bulamaz ve LocalJumpError oluşur. break için de benzer bir durum geçerlidir. Lambdalar ise return ve break ile yalnızca kendilerinden çıktıkları için hiçbir zaman sahipsiz kalmaz. next ise her iki türde de yalnızca o çalıştırmayı bitirir.

arity ve parameters ile imzayı incelemek

arity ve parameters ile imzayı incelemek

arity, proc'un kaç zorunlu argüman beklediğini söyler. İsteğe bağlı argüman varsa sonuç negatif olur ve -n-1 biçiminde hesaplanır (n zorunlu argüman sayısıdır). Belgelere göre bir istisna var: lambda olmayan ve yalnızca sınırlı sayıda isteğe bağlı argümanı olan proc'larda sonuç doğrudan n olur. parameters ise her parametrenin türünü ve adını listeler; normal proc'ta zorunlu görünen parametreler bile :opt olarak raporlanır.

ruby
p proc { |a, b| }.arity       # çıktı: 2
p lambda { |a, b| }.arity     # çıktı: 2
p proc { |a, b = 0| }.arity   # çıktı: 1
p lambda { |a, b = 0| }.arity # çıktı: -2
p proc { |*a| }.arity         # çıktı: -1
p lambda { |x, y = 42, *diger| }.parameters
# çıktı: [[:req, :x], [:opt, :y], [:rest, :diger]]
p proc { |x, y = 42, *diger| }.parameters
# çıktı: [[:opt, :x], [:opt, :y], [:rest, :diger]]

curry ve fonksiyon birleştirme

curry, argümanları parça parça alabilen bir proc döndürür. Yeterli sayıda argüman toplandığında asıl proc çalışır; toplanmadıysa geri kalanını bekleyen yeni bir curried proc döner. Değişken sayıda argüman alan proc'larda kaç argüman beklenmesi gerektiğini curry(n) ile belirtirsiniz. Lambdalarda bu sayı lambdanın imzasıyla uyumlu olmalıdır, aksi hâlde ArgumentError oluşur. >> ve << ise iki çağrılabilir nesneyi tek bir proc'ta birleştirir.

ruby
topla = ->(a, b, c) { a + b + c }
egri = topla.curry
p egri[1][2][3] # çıktı: 6
p egri[1, 2][3] # çıktı: 6
on_ekle = egri[10]
p on_ekle[1][2] # çıktı: 13

carp = proc { |x| x * 2 }
artir = proc { |x| x + 1 }
p (carp >> artir).call(5) # çıktı: 11
p (carp << artir).call(5) # çıktı: 12

carp >> artir önce carp, sonra sonucu artir ile çalıştırır: (5 x 2) + 1 = 11. carp << artir sırayı tersine çevirir: (5 + 1) x 2 = 12. Belgelere göre birleştirilen taraf call metoduna cevap veren herhangi bir nesne olabilir, örneğin bir Method nesnesi.

to_proc ve &:sembol kısayolu

Çağrıda bir nesnenin önüne & koyduğunuzda Ruby o nesnenin to_proc metodunu çağırır ve sonucu blok olarak kullanır. Belgelere göre çekirdek sınıflardan Symbol, Method ve Hash bu metodu uygular. map(&:upcase) kısayolu bu yüzden çalışır: sembol, kendisini alan nesnede aynı adlı metodu çağıran bir proc'a dönüşür. Kendi sınıfınızda to_proc tanımlayarak aynı kolaylığı sağlayabilirsiniz.

ruby
p %w[elma armut].map(&:upcase) # çıktı: ["ELMA", "ARMUT"]
p :to_s.to_proc.call(42)        # çıktı: "42"
[1, 2].each(&method(:puts))
# çıktı: 1
# çıktı: 2
fiyat = { elma: 30, armut: 45 }
p %i[elma kiraz armut].map(&fiyat) # çıktı: [30, nil, 45]

class Selam
  def initialize(on) = @on = on
  def to_proc = proc { |ad| "#{@on}, #{ad}!" }
end
p %w[Ali Ece].map(&Selam.new("Merhaba")) # çıktı: ["Merhaba, Ali!", "Merhaba, Ece!"]

Sembol ve Hash'in diğer özellikleri için Symbol ve String farkı ile Hash metotları yazılarına bakabilirsiniz.

Closure: tanımlandığı ortamı hatırlamak

Proc ve lambda nesneleri closure'dır; oluşturuldukları andaki yerel değişkenleri hatırlar ve kullanmaya devam eder. Bir metodun döndürdüğü lambda, metot bittikten sonra bile metodun parametresine erişebilir. Dıştaki bir değişkeni değiştirirseniz bu değişiklik dışarıdan da görülür.

ruby
def carpan(n)
  ->(x) { x * n }
end
uc_kat = carpan(3)
p uc_kat.call(7) # çıktı: 21

n = 1
ekle = -> { n += 1 }
ekle.call
ekle.call
p n # çıktı: 3

Ruby 3.3 notu: lambda(&proc) artık hata veriyor

Ruby 3.3 NEWS dosyasına göre Kernel#lambda metoduna literal olmayan, lambda niteliği taşımayan bir blok verilirse artık ArgumentError oluşuyor; bu kullanım Ruby 3.0'dan beri uyarı veriyordu. Yani mevcut bir proc'u lambda(&pr) ile lambdaya çeviremezsiniz. Ters yön ise sorunsuzdur: proc, kendisine verilen lambdanın niteliğini korur.

ruby
pr = proc { |x| x }
begin
  lambda(&pr)
rescue ArgumentError => e
  puts "#{e.class}: #{e.message}" # çıktı: ArgumentError: the lambda method requires a literal block
end
p proc(&lambda { |x| x }).lambda? # çıktı: true

Proc ve lambda karşılaştırması

Proc ve lambda karşılaştırması: Oluşturma, lambda?, Eksik argüman, Fazla argüman
ÖzellikNormal procLambda
OluşturmaProc.new, proc, &blocklambda, ->
lambda?falsetrue
Eksik argümannil ile doldurulurArgumentError
Fazla argümanSessizce atılırArgumentError
Tek dizi argümanıParçalarına ayrılırTek argüman sayılır
returnÇevreleyen metottan çıkarYalnızca lambdadan çıkar
breakBloğun verildiği metottan çıkarYalnızca lambdadan çıkar
Sahipsiz kalabilir miEvet, LocalJumpErrorHayır

Sık yapılan hatalar

  • Proc içinde return kullanıp metodun devam etmesini beklemek. Normal proc'taki return metodu bitirir; yalnızca proc'tan çıkmak istiyorsanız next ya da lambda kullanın.
  • Metottan proc döndürüp içinde return bırakmak. Metot bittikten sonra çağrıldığında LocalJumpError alırsınız.
  • Lambdayı map'e & ile verip çift parametre beklemek. Lambda dizi argümanını parçalamaz; ya tek parametre alıp kendiniz ayırın ya da normal bir blok yazın.
  • lambda(&mevcut_proc) ile dönüşüm denemek. Ruby 3.3'ten beri ArgumentError verir.
  • arity değerini argüman sayısı sanmak. Negatif değerler isteğe bağlı argüman olduğunu gösterir; ayrıntı için parameters çıktısına bakın.

Sık Sorulan Sorular

-> ile lambda arasında fark var mı?

Hayır, ikisi de lambda niteliğinde bir Proc üretir. ->(x) { } yalnızca daha kısa bir literal yazımdır.

Ne zaman proc, ne zaman lambda kullanmalıyım?

Belgeler lambdaları, metotlar gibi davrandıkları için kendi başına çalışan fonksiyonlar ve üst düzey fonksiyonlara verilecek argümanlar için uygun bulur. Proc'lar ise iteratör yazmak için, yani argümanların esnek işlenmesi ve return ile metottan çıkılması gereken yerlerde kullanışlıdır.

define_method ile tanımlanan metot proc gibi mi davranır?

Hayır. Belgelere göre define_method'a normal bir proc verseniz bile tanımlanan metot normal metot kurallarıyla argüman kontrolü yapar; bu, lambda niteliğinin korunmadığı tek istisnadır.

Blok parametrelerinde it ve _1 proc için de geçerli mi?

Evet, proc { it * 2 } ve lambda { _1 + _2 } yazılabilir. Ayrıntılar için blok parametreleri yazısına bakın.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: Ruby öğrenme rehberi · Blok ve yield · it ve _1 parametreleri · Metot tanımlama · Keyword argümanları · Symbol ve String farkı