7 İteratörler ve Kapsamlı(generic) for
Bu bölümde kapsamlı for için iteratörlerin nasıl yazılacağını ele alıyoruz. Temel itaratörlerle başlayarak daha basit ve daha efektif iteratörler yazmak için kapsamlı for'un tam gücünü nasıl kullanacağımızı öğreneceğiz.
7.1 Iteratörler ve Closurelar
İteratör, bir veri koleksiyonunun öğeleri üzerinde dolaşmamıza imkan veren herhangi yapıdır. Lua'da iteratörleri tipik olarak fonksiyonlarla temsil ederiz: fonksiyonu her çağırdığımızda koleksiyondan "sıradaki" öğeyi döndürür.
Her iteratörün müteakip çağrılar arasında durum tutması gerekir ki nerede olduğunu ve buradan nasıl ilerleyeceğini bilsin. Closurelar bu görev için mükemmel bir mekanizmadır. Bir closure, çevreleyen ortamından bir kaç yerel değişkene erişen bir fonksiyon olduğunu unutmayın. Bu değişkenler mütakip çağrılar arasında değerlerini tutar ve closure 'ın tüm tur boyunca nerede olduğunu hatırlamasına imkan verir. Tabii ki, yeni bir closure oluşturmak için aynı zamanda yerel olmayan değişkenlerini oluşturmanız gerekir. Bu nedenle bir closure yapısı tipik olarak iki fonksiyon içerir: closure'ın kendisi ve closure'ı yaratan bir fabrika fonksiyon.
Örnek olarak, bir liste için basit bir iteratör yazalım. ipairs'den farklı olarak bu iteratör her öğenin indeksini değil yalnızca değerini döndürür:
function values (t)
local i = 0
return function () i = i + 1; return t[i] end
end
Bu örnekte, values , fabrikadır. Bu fabrikayı her çağırdığımızda yeni bir closure (iteratörün kendisi) oluşturur. Bu closure durumunu t ve i harici değişkenlerinde tutar. iteratörü her çağırdığımızda t listesinden sıradaki değeri döndürür. Son öğeden sonra iteratör bitişi gösteren nil döndürür.
Bu iteratörü bir while döngüsünde kullanabiliriz:
t = {10, 20, 30}
iter = values(t) -- iteratorü yarat
while true do
local element = iter() -- iteratorü çağır
if element == nil then break end
print(element)
end
Ancak, kapsamlı for'u kullanmak daha kolay. Sonuçta bu tür yineleme için tasarlandı:
t = {10, 20, 30}
for element in values(t) do
print(element)
end
Kapsamlı for bir yineleme döngüsü içindeki tüm kayıt tutma işlemlerini yapar: iteratör fonksiyonunu içsel olarak barındırır, bu nedenle iter değişkenine ihtiyacımız yok; her yeni yinelemede iteratörü çağırır; ve iteratör nil döndürdüğünde döngüyü durdurur. (Bir sonraki bölümde kapsamlı for'un bundan daha fazlasını yaptığını göreceğiz.)
Daha gelişmiş bir örnek olarak, 7.1 Listesi, mevcut dosya girdisindeki tüm kelimeleri dolaşan iteratörü gösterir. Bu gezintiyi yapmak için iki değer tutuyoruz: mevcut satır (line değişkeni) ve bu satırdaki konumumuz(pos değişkeni). Bu veriyle sıradaki kelimeyi üretebiliriz daima. iteratör fonsiyonun ana kısmı string.find çağrısıdır. Bu çağrı geçerli konumdan başlayarak geçerli satırda bir kelime arar. Bir veya daha fazla alfanümerik karakterle eşleştirmeyi sağlayacak '%w+' şablonu "kelime" 'i tanımlar. bir kelime bulunursa, fonksiyon, geçerli konumu bu kelimenin ardındaki İlk karaktere güncelleyip bu kelimeyi döndürür(string.subcall verilen pozisyonlar arasında satırdan bir alt string'i ayıklar; Bölüm 20.2'de daha ayrıntılı olarak göreceğiz). Aksi takdirde, iteratör yeni bir satır okur ve aramayı tekrarlar. Daha fazla satır yoksa, yinelemenin sonunu işaret etmek için nil döndürür.
Karmaşıklığına rağmen tumkelimeler 'ın kullanımı basit:
for kelime in tumkelimeler() do
print(kelime)
end
Bu iteratörlerde ortak bir durum: yazmaları kolay olmayabilir ancak kullanımı kolaydır. Bu büyük bir sorun değil; çok değil; Lua programlama nihai kullanıcıları iteratörler tanımlamaz sadece uygulama tarafından bu sağlanan kullanılır.
Liste 7.1. dosya girdisindeki tüm kelimeleri dolaşan iteratör:
function tumkelimeler ()
local line = io.read() -- mevcut satır
local pos = 1 -- satırdaki mevcut pozisyon
return function () -- iterator fonksiyonu
while line do -- satır oldukça tekrarla
local s, e = string.find(line, "%w+", pos)
if s then -- bir kelime mi?
pos = e + 1 -- sonraki pozisyon bu kelimeden sonra
return string.sub(line, s, e) -- kelimeyi döndür
else
line = io.read() -- kelime bulamadınmı; sonraki satırı dene
pos = 1 -- ilk pozisyondan başla
end
end
return nil -- satır kalmadı: gezintinin sonu
end
end
7.2 Kapsamlı for'un Semantiği
Önceki iteratörlerin bir dezavantajı, her yeni döngüyü ilklemek için yeni bir closure oluşturmamız gerekmesidir. Çoğu durumda bu gerçek bir sorun değil. Örneğin, tumkelimeler iteratöründe, tek bir closure oluşturma maliyeti ile tüm dosyayı okuma maliyeti karşılaştırıldığında ihmal edilebilir. Bununla birlikte bazı durumlarda bu yük rahatsız edici olabilir. Bu gibi durumlarda, iteratör durumunu korumak için kapsamlı for 'ın kendisini kullanabiliriz. Bu bölümde kapsamlı for'un durum tutmada sunduğu imkanları göreceğiz.
Kapsamlı for'un döngü sırasında iteratör fonksiyonunu içsel olarak tuttuğunu gördük. Aslında, üç değer tutar: iteratör fonksiyonu, bir değişmeyen durum ve bir kontrol değişkeni. Şimdi ayrıntıları görelim.
Kapsamlı for sözdizimi aşağıdaki gibidir:
for <değişken-listesi> in <ifade-listesi> do
<gövde>
end
Burada, değişken-listesi, virgüllerle ayrılmış bir veya daha fazla değişken isminin bir listesidir ve ifade-listesi, virgüllerle ayrılmış bir veya daha fazla ifadenin bir listesidir. Sık olmasa da, ifade listesi, iteratör fabrikasını çağıran sadece tek bir öğeye sahip olur. Örneğin, şu kodda
for k, v in pairs(t) do print(k, v) end
değişkenler listesi k,v ifadeler listesi tek öğe pairs(t). Genellikle değişkenlerin listesi, bir sonraki döngüde olduğu gibi sadece bir değişkene sahiptir yine:
for line in io.lines() do
io.write(line, "\n")
end
Listedeki ilk değişkene kontrol değişkeni diyoruz. döngü sırasında değeri asla nil olmaz çünkü nil olduğunda döngü biter.
for'un yaptığı ilk şey in'den sonraki ifadeleri değerlendirmektir. Bu ifadeler for tarafından tutulan üç değer olarak hayat bulur: iteratör fonksiyonu, değişmez durum ve kontrol değişkeni için başlangıç değeri. çoklu atamada olduğu gibi, listenin yalnızca son (veya tek) öğesi çoklu değer üretebilir; ki değerlerin sayısı üçe getirilir, ekstra değerler atılır veya gerektiğinde nil'ler eklenir. (Basit iteratörler kullandığımızda, fabrika yalnızca iteratör fonksiyonunu döndürür bu nedenle değişmez durum ve kontrol değişkeni nil alır.)
Bu ilkleme adımından sonra for iteratör fonksiyonunu çağırır iki argümanla : değişmez durum ve kontrol değişkeni. (for yapısı açısından değişmez durumun hiçbir anlamı yoktur. for yalnızca ilkleme adımından iteratör fonksiyon çağrılarına durum değerini geçer.) Daha sonra for, değişken listesi ile bildirilen değişkenlere iteratör fonksiyonu tarafından döndürülen değerleri atar. Döndürülen ilk değer (kontrol değişkenine atanan) nil ise döngü sona erer. Aksi takdirde for gövdesini yürütür ve İşlemi tekrarlayacak iteratör fonksiyonunu tekrar çağırır.
Daha açık olarak şunun gibi bir yapı
for degisken_1, ..., degisken_n in <ifadelistesi> do <blok> end
devamdaki koda eşdeğer:
do
local _f, _s, _degisken = <ifadelistesi>
while true do
local degisken_1, ... , degisken_n = _f(_s, _degisken)
_var = degisken_1
if _degisken == nil then break end
<blok>
end
end
Yani, iteratör fonksiyonumuz f, değişmez durum s ve kontrol değişkeninin başlangıç değeri a0 ise, kontrol değişkeni a1 = f(s, a0), a2 = f(s, a1) ve bu şekilde değerler üzerinde döngüleyecektir ta ki ai nil olana kadar. for başka değişkenlere sahipse bunlar her f çağrısıyla döndürülen ekstra değerleri alırlar.
7.3 Durumsuz iteratörler
Adından da anlaşılacağı üzere kendisi herhangi bir durum tutmayan iteratör durumsuz iteratördür. Bu şekilde yeni closure'lar oluşturma maliyetinden kurtularak çoklu döngüde aynı durumsuz iteratörü kullanabiliriz.
Her iterasyon için for döngüsü iteratör fonksiyonunu çağırır iki argümanla : değişmez durum ve kontrol değişkeni. durumsuz iteratör yalnızca bu iki değeri kullanarak iterasyon için sıradaki öğeyi üretir. Bu tür bir iteratörün tipik bir örneği, bir dizinin tüm öğeleri üzerinde dolaşan ipairs'dir:
a = {"one", "two", "three"}
for i, v in ipairs(a) do
print(i, v)
end
iterasyon durumu, dolaşılan tablo (döngü sırasında değişmeyen değişmez durumdur bu), artı geçerli indeks(kontrol değişkeni). Hem ipairs (fabrika) hem de iteratör oldukça basit; bunları Lua'da aşağıdaki gibi yazabiliriz:
local function iter (a, i)
i = i + 1
local v = a[i]
if v then
return i, v
end
end
function ipairs (a)
return iter, a, 0
end
Lua, bir for döngüsünde ipairs(a) çağırdığında üç değer alır: iteratör olarak iter fonksiyonu, değişmez durum olarak a ve kontrol değişkeninin başlangıç değeri olarak sıfır. Ardından, Lua,1, a[1] (a[1] hazırda nil olmadığı sürece) üretecek iter(a,0) çağırır. İkinci iterasyonda 2, a[2] vb sonuçlarla sonuçlanan iter(a,1) çağırır ve bu şekilde devam eder ilk nil öğeye kadar.
Bir tablonun tüm öğelerini dolaşan pairs fonksiyonu da, iteratör fonksiyonu Lua'da basit bir fonksiyon olan next fonksiyonu dışında benzerdir:
function pairs (t)
return next, t, nil
end
k'nın t tablosunda anahtar olduğu next(t, k) çağrısı tablodaki sıradaki anahtarı, keyfi sıralı, artı ikinci dönüş değeri olarak bu anahtarla ilişkili değeri döndürür. next(t,nil) çağrısı ilk çifti döndürür. Daha fazla çift olmadığında next nil döndürür.
Bazıları pairs çağırmadan doğrudan next kullanmayı tercih eder:
for k, v in next, t do
<döngü gövdesi>
end
for döngüsünün ifade listesinin üç sonuca getirildiğini unutmayın, bu nedenle Lua next, t ve nil alır, ki bu da pairs(t) çağrısıyla alınanla aynı.
Bir bağlı listeyi dolaşmak için bir iteratör, durumsuz bir iteratörün ilginç bir örneğidir. (Daha önce de belirttiğimiz gibi, bağlı listeler Lua'da sık görülmez ancak bazen onlara ihtiyacımız vardır.)
local function getnext (list, node)
return not node and list or node.next
end
function traverse (list) return getnext, list, nil end
Buradaki püf nokta, değişmez durum olarak list'i ana düğüm (traverse tarafından döndürülen ikinci değer) ve kontrol değişkeni olarak geçerli düğümü kullanmaktır. İlk, iteratör fonksiyon getnext çağrıldığında node nil olacak ve böylece fonksiyon ilk düğüm olarak list'i döndürür. Sonraki çağrılarda node nil olmayacak ve böylece iteratör beklendiği gibi node.next 'i döndürecek. Her zamanki gibi iteratörü kullanmak kolay:
list = nil
for line in io.lines() do
list = {val = line, next = list}
end
for node in traverse(list) do
print(node.val)
end
7.4 Kompleks durumlu iteratörler
Sık sık, bir iteratör, birden fazla durum tutmaya ihtiyaç duyar tek değişmez durum ve kontrol değişkeni yetmez. En basit çözüm closure'ları kullanmaktır. Alternatif bir çözüm, bir tabloya ihtiyaç duyulan her şeyi paketlemek ve bu tabloyu iterasyon için değişmez durum olarak kullanmaktır.
Bir tablo kullanılarak, bir iteratör, döngü boyunca ihtiyaçı kadar veri tutabilir. Dahası, bu verileri olduğu gibi değiştirebilir. Durum daima aynı tablo olmasına rağmen (ve bu nedenle değişmez), tablo içeriği döngü boyunca değişir. durumda tüm verilerine sahip olduğu için bu tür iteratörler genellikle kapsamlı for tarafından sağlanan ikinci argüman(iteratör değişkeni) görmezden gelinir.
Bu tekniğin bir örneği olarak, mevcut girdi dosyasından tüm kelimeleri dolaşan tumkelimeler iteratörünü yeniden yazacağız.Bu kez, iki alana sahip bir tablo kullanarak durumunu tutacağız: line ve pos.
iterasyonu başlatan fonksiyon basit. iteratör fonksiyonu ve ilk durumu döndürmelidir:
local iterator -- sonra tanımlanacak
function tumkelimeler ()
local state = {line = io.read(), pos = 1}
return iterator, state
end
iterator fonksiyonu gerçek işi yapar:
function iterator (state)
while state.line do -- satır olduğu sürece tekrarla döngüyü
-- sıradaki kelimeyi ara
local s, e = string.find(state.line, "%w+", state.pos)
if s then -- bi kelime mi buldun?
-- sıradaki konumu güncelle (bu kelimeden sonra)
state.pos = e + 1
return string.sub(state.line, s, e)
else -- kelime bulunamadı
state.line = io.read() -- sonraki satırı dene
state.pos = 1 -- ... ilk konumdan
end
end
return nil -- başka satır yok: döngü sonu
end
Mümkün olduğunca, tüm durumlarını for değişkenlerinde tutan durumsuz iteratörler yazmaya çalışmalısınız. Onlarla bir döngü başlattığınızda yeni nesneler oluşturmazsınız. iterasyonunuzu bu modele sığdıramıyorsanız o zaman closure'ları denemelisiniz. Daha zarif olmanın yanı sıra tipik olarak bir closure, tabloları kullanan bir iteratörden daha etkilidir: birincisi, closure oluşturmak bir tablodan daha az maliyetlidir; ikincisi, yerel olmayan değişkenlere erişim, tablo alanlarına erişimden daha hızlıdır. Daha sonra coroutine'ler ile iteratörler yazmanın başka bir yöntemini göreceğiz. Bu en güçlü çözüm ancak biraz daha masraflı.
7.5 Gerçek iteratörler
iteratör(yineleyeci) ismi biraz yanıltıcı çünkü iteratörlerimiz yineleme yapmıyor: yineleme for döngüsüdür. iteratörler yalnızca iterasyon için ardışık değerler sağlar. Belki daha iyi bir isim “jeneratör” olurdu ancak “yineleyici” hazırda Java gibi diğer dillerde yerleşmiş durumda.
Tabi, gerçekten yineleme yapan iteratörleri oluşturmak için başka bir yol var. Bu tür iteratörleri kullandığımızda bir döngü yazmıyoruz; bunun yerine iteratörün her iterasyonda ne yapması gerektiğini açıklayan bir argümanla iteratörü çağırırız basitçe. Daha spesifik olarak iteratör, döngüsü içinde çağrılan bir fonksiyonu argüman olarak alır.
Somut bir örnek olarak, bu stili kullanarak tumkelimeler iteratörünü bir kez daha yeniden yazalım:
function tumkelimeler (f)
for line in io.lines() do
for word in string.gmatch(line, "%w+") do
f(word) -- fonksiyonu çağır
end
end
end
Bu iteratörü kullanmak için döngü gövdesini bir fonksiyon olarak sağlamalıyız. Eğer sadece her kelimeyi yazdırmak istiyorsak basitçe print'i kullanırız:
tumkelimeler(print)
Genellikle gövde olarak anonim fonksiyon kullanıyoruz. Örneğin sıradaki kod parçası, girdi dosyasında “hello” kelimesinin kaç kez göründüğünü sayar:
local count = 0
tumkelimeler(function (w)
if w == "hello" then count = count + 1 end
end)
print(count)
aynı görev, önceki iteratör stili ile yazılmışı, çok farklı değil :
local count = 0
for w in tumkelimeler() do
if w == "hello" then count = count + 1 end
end
print(count)
Gerçek iteratörler dilin for deyimine sahip olmadığı Lua'nın eski sürümlerinde popülerdi.
jeneratör tarzı iteratörler ile nasıl karşılaştırılırlar? iki stilin de yaklaşık aynı yükü var: iterasyon başına bir fonksiyon çağrısı. Bir tarafta, gerçek iteratörlerle iteratör yazmak daha kolay(ancak bu kolaylığı coroutine'ler ile telafi edebiliriz). Diğer tarafta, jeneratör stili daha esnek. İlk olarak, iki veya daha fazla paralel iterasyona imkan sağlar. (Örneğin iki dosya üzerinde kelime kelime karşılaştırma iterasyonu problemini düşünün.) İkinci olarak, iteratör gövdesi içinde return ve break kullanımına imkan verir . Gerçek iteratör ile return anonim fonksiyondan döndürür, iterasyonu yapan fonksiyondan değil. Velhasıl, genelde jeneratörleri tercih ederim.
Hiç yorum yok:
Yorum Gönder