Evet arkadaşlar, C++ yazılarımıza kaldığımız yerden devam ediyoruz.
Lambda yazım ile temel C++ 11 özelliklerini tamamlamış olacağız. Sonrasında belki STL için de bir yazı yazıp, daha sonra C++ 14/17/20’e yelken açabiliriz. Aslında oldukça geniş konu, fakat ben bu yazımda sizlere temel noktaları aktarmaya çalışacağım. Öncelikle benim gibi bilgisayar bilimleri ile uğraşanlarımız Lambda denilince hemen aklınıza Lambda calculus gelecektir ve bunlar arasında bir ilişki var mı diye de düşünebilirsiniz. Kısa cevap: bir Lisp kadar ilintili değil ama uzun cevap için aşağıdaki sonuçları için google amcamıza danışabiliriz. Bu arada Lisp “Lambda Calculus” ‘ın gerçek anlamda bir gerçeklemesi ve dilin bütün yapısı zaten jenerik programlamayı kolaylaştırmaya yönelik tasarlanmıştır.
Ayrıca “Lambda Calculus” ile ilgili de referanslar bölümüne bir kaç kaynak ekliyorum [6]. Bu konuda daha derinlemesine bilgi almak isteyen arkadaşlarım oraya yönelebilirler.
İçerik
Lambda İfadeleri
Gelelim asıl mevzumuza Lambda İfadeleri. Hepimiz yazılım geliştirirken çeşitli işler için kısa kısa metotlar ve fonksiyonlar yazma ihtiyacı duymuşuzdur, hele de STL ile haşır neşirseniz, özellikle STL algoritma kütüphanesinde bu tarz kullanımlara çok sık ihtiyaç duyarsınız. Bunlar bir iki tane olunca sıkıntı olmasa da, sayıları arttıkça hem kod bu tarz satırlarla dolmakta, okunabilirlik azalmakta ve de bu kodların idamesi zorlaşmaktadır. İşte C++ 11 ile gelen Lambda ifadeleri ile artık:
– Kullanılacak olan fonksiyonlar ihtiyaç duyulan yerlerde tanımlayabilir,
– Yukarıdan aşağıya olan mantıksal ve doğal akışı koruyabilir (fonksiyonlar genelde farklı yerlerde tanımlanırlar, bu sebeple git geller okunabilirliği düşürebiliyor. Özellikle tek seferlik),
– Mevcut kapsam içerisindeki değişkenlere de erişim sunulabilmektedir.
Lambda ifadelerini tanımlayacak olursak. Lambda ifadeleri kısaca anonim fonksiyon olarak ifade edilebilir. Hatta bazı kaynaklar (cppreference) “Mevcut kapsamdaki değişkenleri
yakalayabilen isimsiz fonksiyon nesneleri olarak da tanımlar”. Bir diğer deyişle, lambda ifadeleri aslında çağrılabilecek olan bir kod birimini ifade etmektedirler. C++’dan önce aslında Haskell, C#, Java gibi bir çok dilde buna benzer, ihtiyaç duyulan yerde fonksiyon tanımlamasına olanak sağlayan yapılar sunulmaktaydı (tam ya da daha geniş bir liste için Wikipedia ya alalım sizi).
Meraklılar için lambdaların altında yatan mekanizmayı (derleyici lambda ifadelerini nasıl anlamlandırıyor veya arkada üretilen kod nedir, performansa bir etkisi var mı vs.) yazımın sonuna eklediğim ayrı bir bölümde anlatacağım.
Lambda ifadelerinin genel şablonu aşağıdaki gibi özetlenebilir. Burada * ile işaretlenen kısımlar opsiyonel olan parçalardır.
1 2 3 4 |
[ Yakalama Tanımları ] *(Parametreler) *mutable -> dönüş tipi* { lambda işlevi tanımlaması } |
Detaylara inmeden önce aşağıda basit bazı lambda tanımlamaları ve bunların nasıl çağrıldığına ilişkin bir kaç örnek sizlere aktarayım.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
#include <iostream> using namespace std; int main() { // Parametre almayan ve dönüş yapmayan bir lambda ifadesi auto lambda = []() { cout << "Lambda ifadesi içeren kod!" << endl; }; lambda(); // Yukarıdaki ifade ile aşağıdaki ifadeler tamamen aynı işi yapıyorlar // auto lambda = [] { cout << Code within a lambda expression" << endl; }; // auto lambda = [](void) { cout << Code within a lambda expression" << endl; }; // auto lambda = [](void) -> void { cout << "Code within a lambda expression" << endl; }; // İki parametre alan bir lambda ifadesi örneği auto sum = [](int x, int y) { return x + y; }; cout << sum(5, 2) << endl; // Ayrıca lambda ifadelerini direk çağrıldığı yerde de tanımlayabilirsiniz cout << [](int x, int y) { return x + y; }(5, 2) << endl; } |
Yukarıda auto değişkenleri içerisinde lambda ifadelerine ilişkin işaretçiler tutulmakta. Bir de STL’e ilişkin örneğe bakalım hemen. Önce STL de mevcut durumda 5 ten büyük sayıları nasıl saydıracağımıza bakalım:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
#include <iostream> #include <algorithm> #include <vector> using namespace std; // Bunun yerine fonksiyon nesnesi de oluşturabilirsiniz bool is_greater_than_5(int value) { return (value > 5); } int main() { vector<int> numbers { 1, 2, 3, 4, 5, 10, 15, 20, 25, 35, 45, 50 }; // Besten buyuk sayıları say auto greater_than_5_count = count_if(numbers.begin(), numbers.end(), is_greater_than_5); cout << "5 ten buyuk sayi adeti: " << greater_than_5_count << endl; } |
Şimdi de aynı işi lambdalar ile yapalım. Siz hangisi tercih edersiniz 🙂
1 2 3 4 5 6 7 8 9 10 11 12 |
#include <iostream> #include <algorithm> #include <vector> using namespace std; int main() { vector<int> numbers { 1, 2, 3, 4, 5, 10, 15, 20, 25, 35, 45, 50 }; // Besten buyuk sayıları say cout << "5 ten buyuk sayi adeti: " << count_if(numbers.begin(), numbers.end(), [](int x) { return (x > 5); }) << endl; } |
Şimdi biraz daha derinlere inebiliriz. Önce lambdalar ile değişken yakalama mekanizmasına bakacağız daha sonra da parametre geçirme ve dönüş mekanizmalarını inceleyeceğiz. Bu arada lambda ifadelerinin arka planı için ise son bölüme başvurabilirsiniz.
Lambda ifadeleri ile değişken yakalama (“Variable Capture Mechanism”):
Normal fonksiyonlardan farklı olarak lambda ifadeleri, mevcut tanımlandığı blokta/kapsamda (“scope”) tanımlanmış olan değişkenlere de erişim sağlayabilmektedir. Buna “Variable Capture Mechanism” diğer bir ifade ile Değişkenleri Yakalama Mekanizması diyebiliriz.
Bu mekanizma ile aşağıdaki şekillerde değişkenleri yakalayabiliriz:
– Değer/Kopya yakalama (“Capture by value”)
– Referans yakalama (“Capture by reference”)
– Hem değer hem de referans yakalama (“Capture by both value and reference (mixed capture) “)
Detaylarını örnekler ile aşağıda anlatacağım ama özetleme adına bu kullanımlara ilişkin yazım şekillerinin özetini aşağıda bulabilirsiniz:
- [&] : tanımlandığı kapsam içerisindeki bütün değişkenlerin referansları yakalanır/kullanılabilir (capture all external variables by reference)
- [=] : tanımlandığı kapsam içerisindeki bütün değişkenlerin kopyaları yakalanır/kullanılabilr (capture all external variables by value)
- [a, &b] : a değişkeninin kopyasını, b değişkeninin de referansını yakalar/kullanır
- [] : herhangi bir ortam değişkeni yakalanmaz ve sadece lambda ifadeleri içerisindeki yerel değişkenlere erişiminiz olur
Şimdi bu zamana kadar yazdıklarımıza ilişkin yakama örnekleri üzerinden kullanımlarına bakalım.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
int x=1, y=1, z=1; // 1. Ornek [ ] ( ) { cout << “Example 1”; } ( ); // 2. Ornek [ ] ( ) { cout << x; } (); // 3. Ornek [x] ( ) { cout << x+1; } (); // 4. Ornek [=] ( ) { cout << x << y; } (); // 5. Ornek auto tmp = [ ] ( ) { cout << "merhaba"; } (); |
Yukarıdaki 1. örnekte herhangi bir değişken yakalanmadığı duruma örnek (kullanım 4). 2. örnek ise geçerli bir tanımlama değil, çünkü x değişkeni lambda ifadesine herhangi bir şekilde geçirilmemiş. 3. örnek bir önceki örneğin geçerli hali ve sadece kopyasının geçirildiği duruma örnek teşkil etmektedir. 4. örnek de benzer şekilde z dışındaki bütün değişkenler lambda ifadesi içerisine kopyalanarak geçirilmekte. z kullanılmadığı için geçirilmemektedir. Son örnek te geçerli bir satır değil. Neden sizce? Biraz düşünün sebebi yazının sonunda (ipucu diğer satırlardan farkı nedir “()” ne yapar).
Şimdi biraz da değişkenlerin referans olarak yakalanmasına ilişkin bir kaç örnek inceleyelim.
1 2 3 4 5 6 7 8 9 10 |
int x=1, y=1, z=1; // Bütün değişkenler referans olarak geçirilmekte [&] ( ) { cout << x << y << z; }( ); // z referans olarak geçirilmekte, diğerlerinin kopyası geçirilmekte [=,&z] ( ) { z = x + y; }( ); // x 'in kopyası geçirilmekte, diğerleri referans olarak geçirilmekte [&,x] ( ) { y = x; z = x; }( ); |
Bu kullanımların yanında lambda ifadelerini sınıf metotları içerisinde de kullanabilirsiniz. Fakat burada bir hususa dikkat etmeniz gerekiyor. Lambdaların her bir için derleyici özel ve ayrı bir sınıf oluşturmakta ve bu sebeple çalışma zamanında hepsinin kendi kapsamları oluyor. Bir diğer ifade ile sınıf metotları içerisindeki lambda ifadeleri içerisinde bulundukları sınıf değişkenlerine ulaşamazlar. Bunun için ilgili sınıfın işaretçisi “this” ile geçirilmesi gerekmektedir. Hemen bir örnek ile inceleyelim:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class Filtreleyici { public: Filtreleyici(vector<int>& src) : mDataToFilter(src) { } void Filter() { remove_if(mDataToFilter.begin(), mDataToFilter.end(), [this](int i){ return ( i < mFilterReference); }); // Asagidaki kullanim gecerli degil // [](int i){ return ( i < mFilterReference); }); private: vector<int> mDataToFilter; int mFilterReference; }; |
Parametre Geçirme ve Dönüş Değerleri:
Kapsam yakalama ve değişkenlerin lambda ifadelerine geçirilmelerinden sonra lambda ifadelerine parametrelerin geçirilmesi hususuna eğilelim. Daha önce de bahsettiğim gibi lambda ifadelerine normal fonksiyonlar gibi parametreler geçirebiliyoruz. Lambda ifadeleri için de normal fonksiyonlara uygulanan parametre geçirme kuralları uygulanmakta. C++ 14 e kadar normal fonksiyonlardan farklı olarak varsayılan parametre değerlerini lambdalarda kullanamıyoruz, C++ 14 de bu kısıtlama da ortadan kalkmakta. Lambda ifadelerindeki parantezlerde aslında isteğe bağlı eğer herhangi bir parametre geçirilmiyor ise “[] {}” tamamen geçerli bir kullanım (hatta en basit kullanım diyebiliriz).
Lambda ifadelerinde normal fonksiyonlarda olduğu gibi değerler dönülebilmektedir. Fonksiyonlardan farklı olarak, lambdalarda dönüş değerleri direk ifade edilebilmeleri yanında derleyici tarafından otomatik olarak da belirlenebiliyor. Eğer lambda tanımlaması içerisinde birden fazla dönüş ifadesi var ise ya da dönüş tipi çıkarımında bulunamayacağı durumlarda dönüş tipi de lambda ifadesinde belirtilmelidir. Ayrıca yazılan kodta bir dönüş işlemi “return deyimi” yok ise lambda işlevin geri dönüş değerinin türü void kabul edilir.
Not olarak lambda ifadelerinde throw kullanımı da mümkün (normal C++ standardında throw kullanımı “depreceated” olarak işaretlense de dilden tamamen henüz çıkarılmadı).
Şimdi bu son hususlara ilişkin örnek kodları inceleyelim:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
// İki tane kopyalama yöntemi kullanılarak parametreler geçiriliyor // Bu durum için dönüş değeri belirtmeye gerek yok (int olarak derleyici kendisi bu çıkarımda bulunabilir) [ ] (int x, int y) {return x + y; } (1,2); // Hatalı ifade // İlk parametre referans olarak geçirileceği ifade edilmiş ve bu rvalue parametreye sabit bir değer geçirilmiş [ ] (int &x, int y) {return x + y; } (1,2); // Lambda ifadesi ilk parametreyi sabit referans olarak alıyor ikincinin ise kopyasını alıyor // Bu durumda da dönüş tipi belirtilmeli [ ] (const int &x, int y) -> int {int z = 3; return x + y + z; } (1,2); // Yerel de tanımlanmış bir değişken referans olarak dönülmüş (tabi bu doğru ve tercih edilen bir kullanım değil ;) [ ] (int x, int y) -> int& {int z = 0; return z; } (1,2); Birazda dönüş değerlerine ilişkin örnekleri inceleyelim: // Açıkça dönüş değeri belirtildiği durum int p1 = [ ] (int x, int y) -> int {return x+y;} (1,2); // Dönüş değeri belirtilmeyen durumlar. Burada derleyici tipi belirliyor (int) int p2 = [ ] (int x, int y) -> {return x+y;}(1,2); // Derleme hatası. Belirtilen ve dönülen tipler arasında uyumsuzluk var int p4 = [ ] () -> int {return false;}(); // Derleme hatası. Belirtilen kodtan dönüş değerinin belirlenmesi mümkün değil. // Açıkça tipinin ifade edilmesi gerekiyor int p5 = [ ] (int x) { if x > 5 return x; else return true;}(1); |
STL Kullanım:
Yukarıda aslında STL’e kullanıma ilişkin bir kaç örnek vermeye çalıştım. Aşağıda çok bilindik sıralama fonksiyonu için olan kullanıma bakalım:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// Klasik yaklaşım class comparator { public: bool operator() (int x, int y) const { return x > y; } }; sort (v.begin(), v.end(), comparator()); // Lambda kullanımı ile gelen yeni yaklaşım sort(v.begin(), v.end(), [ ] (int x, int y) { return x > y;}); |
Bu kullanım yanında lambda ifadeleri şablonlara (“template”) parametre olarak ta geçirilebilirler. Aşağıda buna ilişkin bir örnek kullanım görebilirsiniz:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
#include <set> #include <functional> #include <iostream> int main() { // recommended std::set< int, std::function< bool(int,int) > > exampleSetWithLambda([] ( int x, int y ) { return x > y ; } ) ; exampleSetWithLambda.insert(2); exampleSetWithLambda.insert(1); exampleSetWithLambda.insert(5); std::cout << "Sirali:" << std::endl; for(auto item : exampleSetWithLambda) { std:: cout << item << " "; } std::cout<< std::endl; std::set< int > exampleSetWithNoLambda; exampleSetWithNoLambda.insert(2); exampleSetWithNoLambda.insert(1); exampleSetWithNoLambda.insert(5); std::cout<< "Sirasiz:" << std::endl; for(auto item : exampleSetWithNoLambda) { std:: cout << item << " "; } } |
Bunların yanında std::function kullanarak lambda lari metotlara da geçirebilirsiniz.
C++ Lambda larının Çalışma Mekanizması
Lambda’lar yazımın başında da bahsettiğim üzere aslında çağrılabilir küçük kod parçaları olarak adlandırabiliyor. Diğer fonksiyonlardan farklı olarak içerisinde bulunduğu kapsamda tanımlı olan değişkenlere erişim de sağlayabilmekte. Peki mevcut mekanizmalardan farkı nedir? Yani normal fonksiyon, functor sınıflarından ( operator() tanımlayan sınıflar) ne farkı var? Bu başlıkta buna bakacağız.
Basit olarak aslında siz bir lambda tanımlaması yaptığınız zaman, derleyici arka planda sizler için bir “functor” sınıfı oluşturuyor. Bu sınıfların her biri eşsiz ve her tanımlama için ayrı ayrı oluşturuluyor. Örneğin:
1 |
[](X& elem) { elem.op(); } |
için derleyici:
1 2 3 4 5 6 7 8 |
class _DerleyiciTarafindanAtanmisIsim_ { public: void operator()(X& elem) const { elem.op(); } }; |
tanımı oluşturuyor. Bu bir anlamda “syntatic sugar” dediğimiz yani işimiz kolaylaştıran bir mekanizma olarak düşünebiliriz. Şimdi biraz daha derine inelim ve assembly kodlarını karşılaştıralım :).
Önce hiç bir değişkenin yakalanmadığı duruma bakalım. Bu arada bu kodlar (x86-64 gcc 8.2 derleyicisinden çıkan kodlar).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
int function (int a) { return a + 3; } class Functor { public: int operator()(int a) { return a + 3; } }; int main() { auto lambda = [] (int a) { return a + 3; }; Functor functor; volatile int y1 = function(5); volatile int y2 = functor(5); volatile int y3 = lambda(5); return 0; } |
Normal fonksiyon tanımı için aşağıdaki gibi bir kod üretilmekte:
1 2 3 4 5 6 7 8 |
traditionalFunction(int): push rbp mov rbp, rsp mov DWORD PTR [rbp-4], edi mov eax, DWORD PTR [rbp-4] add eax, 3 pop rbp ret |
“Functor” için ise aşağıdaki gibi kod üretilmekte:
1 2 3 4 5 6 7 8 9 |
Functor::operator()(int): push rbp mov rbp, rsp mov QWORD PTR [rbp-8], rdi mov DWORD PTR [rbp-12], esi mov eax, DWORD PTR [rbp-12] add eax, 3 pop rbp ret |
Lambda için ise aşağıdaki gibi:
1 2 3 4 5 6 7 8 9 |
main::{lambda(int)#1}::operator()(int) const: push rbp mov rbp, rsp mov QWORD PTR [rbp-8], rdi mov DWORD PTR [rbp-12], esi mov eax, DWORD PTR [rbp-12] add eax, 3 pop rbp ret |
herhangi bir değişken yakalanmadığı durumda görüleceği üzere “functor” ile lambda ifadelerine ilişkin üretilen kod aynı. Standart metot ile ise ufak bir fark var.
Şimdi ortam değişkenlerini kopyalayarak yakalama durumunu inceleyelim. Bu durumda normal metotları kullanamayacağız. Sadece “functor” lar ile lambdaları karşılaştıracağız.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
class Functor { public: Functor(const int x) : m_x(x) { } int operator()(int a) { return a + m_x; } private: int m_x; }; int main() { int x = 3; auto lambda = [=] (int a) { return a + x; }; Functor functor(x); volatile int y1 = functor(5); volatile int y2 = lambda(5); return 0; } |
Burada fonksiyon nesneleri için bizi ilgilendiren iki metot var. Bunlar yapıcı ve () operatörü. Şimdi bunlar için
üretilen kodlara bakalım:
1 2 3 4 5 6 7 8 9 10 11 |
Functor::Functor(int): push rbp mov rbp, rsp mov QWORD PTR [rbp-8], rdi mov DWORD PTR [rbp-12], esi mov rax, QWORD PTR [rbp-8] mov edx, DWORD PTR [rbp-12] mov DWORD PTR [rax], edx nop pop rbp ret |
Yapıcıya ilişkin kod kısaca aslında esi yazmacı içeriğini rdi yazmacı ile ifade edilen belleğe kopyalamaya karşılık geliyor.
Buna geçirilen değerleri görmek için de main() içerisinde buna ilişkin üretilen koda bakalım.
1 2 3 4 5 6 7 8 9 |
// int x = 3; mov DWORD PTR [rbp-4], 3 // Functor functor(x); mov edx,DWORD PTR [rbp-0x4] lea rax,[rbp-0x20] mov esi,edx mov rdi,rax call Functor::Functor(int) |
Buradan x’in rbp-0x4 yazmacında saklandığını ve daha sonra dolaylı yoldan esi yazmacına yazıldığını görüyoruz. rbp-0x20 adresini rdi atıldığını görüyoruz. lea komutu ve sonrasındaki satırlar ile bizim fonksiyon nesnemizi saklayan adres.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
Functor::operator()(int): // int operator()(int a) push rbp mov rbp, rsp mov QWORD PTR [rbp-8], rdi mov DWORD PTR [rbp-12], esi // return a + m_x; mov rax, QWORD PTR [rbp-8] mov edx, DWORD PTR [rax] mov eax, DWORD PTR [rbp-12] add eax, edx pop rbp ret |
Asıl () operatörü için üretilen kod ile yapıcı için üretilen kod birbirine oldukça benzemekte. En büyük fark ise m_x in değerini okumak için çağrılan iki satır komut.
Şimdi lambda ifadesi için üretilen koda bakalım:
1 2 3 4 5 6 7 8 9 10 11 |
main::{lambda(int)#1}::operator()(int) const: push rbp mov rbp, rsp mov QWORD PTR [rbp-8], rdi mov DWORD PTR [rbp-12], esi mov rax, QWORD PTR [rbp-8] mov edx, DWORD PTR [rax] mov eax, DWORD PTR [rbp-12] add eax, edx pop rbp ret |
Bu kod parçasının da fonksiyon nesne objesi için üretilen ile aynı olduğu görülebilir. Burada tabi lambda ifadesinin oluşturulmasına ilişkin kodunun nerede olduğu sorulabilir.
Onun için main içerisine bakıyoruz:
1 2 |
mov eax, DWORD PTR [rbp-4] mov DWORD PTR [rbp-16], eax |
Görüleceği üzere fonksiyon nesne objesinin yapıcısı için üretilen kod, lambda için üretilen kodtan oldukça fazla. Bunun da sebebi yapıcı için olan oluşturucu kodu normal üretilen kodun içerisine gömülmekte. Ortam değişkenlerinin referans olarak geçirildiği durumda aslında yukarıdakine benzer tek fark değer yerine işaretçilerin geçirilmesi.
Bu kullanımlara baktığımızda özellikle fonksiyon nesneleri ile lambda ifadeleri birbirlerinin hemen hemen aynısı. Ana farklılıklar:
– “functor” lar ve lambda ifadeleri fazladan bir this göstergeci geçiriyorlar (fazladan 8 byte),
– Lambda ifadelerine ilişkin yapıcı kodları lambda ifadelerinin içerisine yediriliyor ve bu sayede kopyalamaya ilişkin fazladan oluşturulan kod miktarı azaltılmış oluyor.
Sonuç olarak lambda ifadelerinin performans anlamında bir yük getirmediğini ifade edebiliriz ([8] de daha detaylı bir performans karşılaştırması görebilirsiniz).
Yine kısa dedik sözü uzattık 🙂 olsun lambda ifadeleri oldukça önemli ve nispeten yeni bir kabiliyet, siz yazılımperverlere faydası olduysa ne ala 🙂
Kaynaklar:
- https://www.cprogramming.com/c++11/c++11-lambda-closures.html
- https://www.geeksforgeeks.org/lambda-expression-in-c/
- https://web.mst.edu/~nmjxv3/articles/lambdas.html
- https://blog.feabhas.com/2014/03/demystifying-c-lambdas/
- https://www.wikiwand.com/en/Lambda_calculus
- https://www.inf.fu-berlin.de/lehre/WS03/alpi/lambda.pdf
- https://en.cppreference.com/w/cpp/language/lambda
- https://vittorioromeo.info/index/blog/passing_functions_to_functions.html
Faydalı bir yazı olmuş
Teşekkürler
Geri bildiriminiz için çok teşekkürler.
teşekkür ederim benim için çok faydalı oldu. Cümleleriniz anlatım tarzını çok güzel.
Türkçe kaynak oluşturduğunuz ve -blokta/kapsamda (“scope”)- bu tarz bilgisayar terimlerini türkçeleştirerek kullandığınız içinde ayrıca teşekkür ederim. Kaliteli içerikleri okumanın keyfi bir başka oluyor.
emeğinize sağlık 🙂
Faydalı oldu ise ne mutlu. Güzel geri bildiriminiz için ben teşekkür ederim.