Fonksiyonel Programlama Nedir? (JavaScript ES6 Üzerinden Anlatım)
Fonksiyonel programlamayı ilk duyduğumuzda, adından da anlaşılacağı üzere fonksiyonlar ile işler yapan bir programlama türü olduğunu düşünürüz. Elbette ki bunda haklıyız, fakat bunun daha ötesinde anlamı ve amacı olan unsurları ihtiva etmektedir. İsterseniz gelin şimdi bunlara değinelim.
Fonksiyonel programlama nedir?
Fonksiyonel programlama, bir uygulamanın state’ini (durumunu) ve verilerini direkt olarak değiştirmeden, matematiksel fonksiyonlar yardımıyla sonucun üretilmesini sağlayan bir programlama paradigmasıdır. Emirsel (imperative) programlama dillerinde, değişkenler üzerinde atamalı ifadelerle yapılan işlemlerin yerine fonksiyonel programlama dillerinde deklaratif işlemler yapılır. Bu nedenle fonksiyonel dillerde değişkene değer atama işlemi bulunsa dahi, tekrar değişkenin değerini değiştirme gibi operasyonlara izin verilmez. Bunun yerine ilgili değişkenin değeri kopyalanarak yeni bir değişken üretilir. Bu sayede çok thread’li işlemlerde, aynı kaynağa erişmekten dolayı kilitlenmenin oluşması engellenir.
Fonksiyonel programlamada, çıktı değeri yalnızca girdi olarak aldığı parametre değerlerine bağlıdır. Bu sayede bir fonksiyonu, aynı parametreler ile defalarca kez çağırsanız dahi aynı sonucu alırsınız. Bunun aksine emirsel programlamada ise, fonksiyon parametrelerinin yanısıra programın global değişkenleri de çıktı değerine etki edebilir. Bu tarz yan etkileri gidererek, uygulama kodunun daha anlaşılır hale getirilmesi, fonksiyonel programlamanın en önemli hedeflerinden biridir.
Fonksiyonel programlamanın kökleri, aslında 1930’lu yıllarda matematiksel işlemlerde fonksiyonların soyutlaştırılması için geliştirilen lambda calculus sistemine kadar uzanır. Bu sistem sayesinde; hesaplanabilirlik, Entscheidungsproblem, fonksiyon tanımlama, fonksiyonun uygulanması ve öz yinelemeli (recursive) işlemlerin modellenmesi sağlanır. Fonksiyonel programlama dillerinin birçoğu da, lambda calculus sisteminin koda dökülmüş halini ifade etmektedir.
Emirsel programlamada, uygulamanın state değişikliğine yol açan ifadeler en basit haliyle =
yardımıyla yapılan atamalardır. Ayrıca matematiksel olmayan diğer alt fonksiyonları da çağırabilirler (Örn: dosyaya yazma). Bu fonksiyonlar, geriye bir değer döndürmese bile dosyaya yazma gibi yan etkili işlemler gerçekleştirebilirler. Bu nedenle çalışan programın o anki durumuna göre, aynı parametrelere sahip bir fonksiyon, farklı zaman dilimlerinde farklı sonuçlar üretebilir. Bundan dolayı emirsel programlama dilleri başvurusal şeffaflığa (referential transparency) sahip değildir.
Fonksiyonel programlama dilleri, uzun yıllar boyunca yazılım sektöründen ziyade akademik alanda yaygın olarak kullanılmaktadır. Bununla beraber, C# ve Java gibi programlama dillerine fonksiyonel programlama yeteneklerinin eklenmesi ile yazılım endüstrisinde de yerini almaktadır. List, Scheme, Clojure, Wolfram, Racket, Erlang, OCaml, Haskell ve F# uygulama geliştiriminde yaygın olarak kullanılmaktadır. Dünyada en çok kullanılan JavaScript dili ise, emirsel ve nesne yönelimli yeteneklerinin yanında, dinamik tipli fonksiyonel dil olma özelliğine de sahiptir. Ayrıca fonksiyonel programlama, bazı dillerin belirli sektörlerde ve çalışma alanlarında başarıya ulaşmaları için de önemlidir. Örneğin istatistikte R dili, finansal analizde J, K ve Q dilleri, XML işlemede ise XQuery/XSLT yaygın olarak kullanılmaktadır. SQL ve Lex/Yacc gibi diğer çalışma alanına yönelik diller ise mutable (değiştirilebilir) değerleri desteklemeyen bazı fonksiyonel programlama özelliklerini içerirler.
Kotlin, Perl, PHP ve C++11 gibi fonksiyonel olmayan dillerde de, fonksiyonel tarzda programlama gerçekleştirilebilir. Bunun yanında, sıklıkla fonksiyonel tarzda geliştirim yapılan Scala’da ise durum biraz ilginçtir. Çünkü Scala dili List elemanındaki drop() fonksiyonu gibi yan etkili fonksiyonların bulunmasından ve uygulamanın state’inin değiştirilebilmesinden dolayı, emirsel ve fonksiyonel programlama dilleri arasındaki gri bölgede kalmaktadır.
Fonksiyonel dillerdeki bazı kavramlar
Fonksiyonel programlama dillerindeki bazı kavramlar yalnızca bu dillere özgüdür ve emirsel programlama ile nesne yönelimli programlamada bu kavramlar bulunmaz. Ancak günümüzdeki modern programlama dilleri birçok farklı programlama türünü (multiparadigm) bünyesinde barındırdığı için (örneğin JavaScript, C#, Java), yazılımcılar da fonksiyonel özelliklere sahip emirsel dilleri tercih etmektedir. Şimdi isterseniz fonksiyonel programlamaya yönelik kavramlara bir göz atalım.
Referential transparency (Başvurusal şeffaflık)
Fonksiyonun sadece girdi değerini alıp, global bir değişkene etki etmeden çıktı değerlerini üretmesidir. Bu fonksiyonlar, dışarıdan gelecek yan etkilere maruz kalmadığı için her çağrışınızda aynı sonucu üretirler. Bu özelliğe sahip fonksiyonlara pure functions (saf fonksiyonlar) denilmektedir. JavaScript üzerinden örnek vermek gerekirse, aşağıdaki toplama işlemini yapan fonksiyon bu özelliğe uymaktadır.
var topla = (a, b) => { return a + b }
Peki bunun tam tersi yani impure (saf olmayan) fonksiyon olacak şekilde bir örnek üretelim.
var globalDegisken = 2;
var toplaVeGlobalEkle = (a, b) => { return a + b + globalDegisken }
Test edecek olursak:
toplaVeGlobalEkle(1,2,3)
//<< 6
Bu fonksiyonun vereceği sonuç belirli (6
) olduğu için, fonksiyon çağrımı yerine direkt olarak 6’yı yazabiliriz. Buna substitution model (yerine koyma modeli) denir. Bu sayede güvenli bir şekilde paralel çalıştırma ve cache’leme yapılabilir. Çünkü global bir değişkene ihtiyaç olmadığı için thread’ler arası lock olayı oluşmaz ve senkronizasyon işlemine gerek kalmaz. Ayrıca cache’leme konusuna gelecek olursak, fibonacci serisini üreten bir fonksiyonun tekrar tekrar çalıştırılıp sonucu sıfırdan üretmesi yerine, parametre ve sonuç ikilileri bir dizi içerisinde saklanarak önceki sonuç tekrar kullanılıp hesaplama adımları kısaltılabilir. Yani fibonacci(5)’in 24 olduğunu kaydettiysek, fibonacci(6) için sadece 24 * 6
işleminin uygulanması yeterli olacaktır. Referential transparency, paralellik ve cache’leme özellikleri kazandırdığı için önemlidir.
Declarative (bildirici) ve abstraction (soyutlama) kavramları
Fonksiyonel programlama, problemin soyutlaştırılan (abstracted) bir çözümünü yazarken bildirici (declarative) olmayı gerektirir. Bunun tam tersi ise imperative (emredici, emirsel) kod yazma biçimidir. Bunun için öncelikle aşağıdaki örneği ele alalım:
var dizi = [1, 2, 3, 4, 5];
var diziToplami = 0;
for(var i = 0; i < dizi.length; i++) {
diziToplami = diziToplami + dizi[i];
}
console.log(diziToplami); // 15
Bu örnekte bir dizinin tüm elemanlarının değerlerinin toplanıp ekrana yazdırılmıştır. Örnek sorunsuz bir şekilde çalışmaktadır. Fakat buradaki problem, derleyiciye direkt olarak ne yapması gerektiğinin belirtilmesidir: dizinin uzunluğu hesapla, dizi üzerinde teker teker gez, dizideki index değeri üzerinden her elemanın değerini bul vb. Buna imperative (emirsel) programlama denir. Emirsel programlamada her işin “nasıl” yapılacağı derleyiciye tek tek belirtilir.
Deklaratif programlamada ise derleyiciye işin “nasıl” yapılması değil, “ne” yapması gerektiği bildirilir. İşin “nasıl” yapılacağı ise, bazı genel fonksiyonlar (higher-order fonksiyonlar) ile soyutlaştırılır. Örneğin üstteki örneği, JavaScript bünyesinde yer alan Array.prototype.forEach() fonksiyonu ile soyutlaştırabiliriz:
var dizi = [1, 2, 3, 4, 5];
var diziToplami = 0;
dizi.forEach((eleman) => diziToplami += eleman )
console.log(diziToplami); // 15
Burada önceki sonuç ile aynı sonucu ürettik. Fakat işin “nasıl” yapılması gerektiğini forEach fonksiyonu ile soyutlaştırmış olduk. Ayrıca daha kısa kod yazarak, oluşabilecek hata oranını azalttık. Fonksiyonel programlama, bu şekilde soyutlaştırma ile fonksiyonların oluşturulmasıdır. Bu sayede kod içerisinde tekrar tekrar kullanılabilen kod parçacıkları elde edilir.
Closure’lar nedir?
Closure en basit haliyle bir fonksiyonun içerisinde tanımlanan diğer bir fonksiyondur. Örnek verecek olursak:
function a(){
function closureFonksiyonu() {
//…
}
}
Closure fonksiyonlar aşağıdaki scope’lara erişebilirler:
- Kendi içerisinde tanımlanan değişkenlere,
- Üst fonksiyonların içindeki değişkenlere,
- Global değişkenlere.
Örnek:
const globalDegisken = “GLOBAL”;
function disFonksiyon() {
let disDegisken = “Dış değişken”;
function icFonksiyon() {
var icDegisken = 5;
console.log(icDegisken); // 1. scope’a erişiyor
console.log(disDegisken); // 2. scope’a erişiyor
console.log(globalDegisken); // 3. scope’a erişiyor
}
icFonksiyon();
}
İç içe oluşturularak istenilen herhangi bir ebeveyn foknsiyonun da değişkenlerine erişim yapabilirler:
function a() {
var x = 1;
function b() {
var y = 2;
function c() {
console.log(x, y);
}
c();
}
b();
}
// 1 2
Closure’lar, kendisini kapsayan fonksiyonların scope’una erişebilmeleri sayesinde higher order fonksiyonların oluşmasını mümkün kılmaktadırlar.
Higher order fonksiyonlar nedir?
High order fonksiyonlar (yüksek mertebeli fonksiyonlar), diğer fonksiyonları parametre olarak alır veya sonuç olarak bu fonksiyonları geri döndürürler. Bunu, matematikte diferansiyel denklemlerde kullanılan d/dx operatörü gibi düşünebilirsiniz. Çünkü d/dx operatörü de benzer şekilde bir f denklemine uygulandığında, çıktı olarak yeni bir fonksiyon üretmektedir.
Higher order fonksiyonlar genel haliyle yaygın olarak yer alan problemlerin soyutlaştırılması için yazılırlar. Yani başka bir deyişle higher order fonksiyonlar, soyutlamaların tanımlanmasını sağlayarak, geliştiriciler için basit fonksiyon API’leri oluşturulmasına olanak verirler. Aşağıdaki forEach fonksiyonunu örnek alacak olursak:
// Higher-order fonksiyon tanımı
var forEach = (dizi, f) => {
for(var i = 0; i < dizi.length; i++) {
f(dizi[i]);
}
}
// Fonksiyon çağrımı:
forEach([1, 2, 3], console.log);
//<< 1
//<< 2
//<< 3
Bu fonksiyonda parametre olarak bir dizi ve dizinin elemanları üzerinde uygulanacak fonksiyon yer almıştır. Bu fonksiyon higher order bir fonksiyon olup, dizi üzerinde gezme işlemini soyutlaştırmıştır. Başka bir deyişle, fonksiyonu kullanan diğer bir yazılımcının, dizi üzerinde gezme işleminin nasıl kodlandığını bilmesine gerek yoktur. Sadece çalışma mantığını bilmesi yeterlidir. Böylelikle, problem soyutlaştırılmış hale gelir. High order fonksiyonlar, partial application ve currying denilen teknikleri mümkün kılar.
Currying nedir?
Currying, birçok argümana sahip fonksiyonun, tek argümanlı fonksiyonlar hale getirilip, iç içe birbirini çağıracak şekle getirilerek çalıştırılmasına denir.
Böylece, fonksiyonun tüm parametrelerini tek seferde alması yerine, önce ilk parametre alınıp bir fonksiyon döndürülür, sonra ikinci parametre alınıp bir fonksiyon döndürülür ve bu şeklinde tüm parametreler karşılanana dek silsile şeklinde devam edecek hale getirilir.
Örneğin 3 adet parametreyi toplayan bir fonksiyonumuz topla(a,b,c)
ise bunun curried hali topla(a)(b)(c)
olacaktır. Bu teknik kullanılarak, daha küçük, sade ve daha kolay yapılandırılabilir fonksiyonlar elde edilir. Örnek olarak:
const topla = (a,b,c) => a + b + c;
Bu fonksiyonu curried hale getirecek olursak:
const toplaCurried = a => b => c => a + b + c;
Üstteki toplaCurried fonksiyonu normalde üç girdi değeri ile çalışacaktır. Eğer tek bir girdi değeri verirsek çıktı olarak bir fonksiyon geri döndürür:
toplaCurried(3)
//<< b => c => a + b + c
Burada a değeri closure konsepti kullanılarak capture edilmiştir ve henüz kullanılmamaktadır. Diğer girdi değerlerini de verdiğimizde olağan şekilde çalışacaktır.
toplaCurried(3, 2, 4)
// << 9
Şimdi curry fonksiyonunu kullanarak, topla fonksiyonunun curried halini oluşturabiliriz:
let toplaOtomatikCurried = curry(topla);
toplaOtomatikCurried(2)(3)(4)
// << 9
Currying’in gerçek hayatta kullanımı
Currying’in gerçek hayatta ne gibi bir yararının olduğunu daha iyi anlamak için aşağıdaki log
fonksiyonu örneğini ele alalım:
const log = (mode, title, msg, line) => {
switch (mode) {
case “ERROR”:
console.error(title, msg + ” at line: “ + line);
break;
case “WARN”:
console.warn(title, msg + ” at line: “ + line);
break;
case “INFO”:
console.info(title, msg + ” at line: “ + line);
break;
default:
console.log(title, msg + ” at line: “ + line);
}
}
Diyelim ki bu log fonksiyonunu Network.js gibi bir dosyada ağ hatalarını konsola basmak için kullanacağız. Parametre çağrımı aşağıdaki gibi olacaktır:
log(“ERROR”, “Error at Network.js”, “Cannot readproperty”, 45);
log(“ERROR”, “Error at Network.js”, “‘undefined’is not an object”, 132);
log(“ERROR”, “Error at Network.js”, “null is notan object”, 69);
log(“ERROR”, “Error at Network.js”, “‘undefined’is not a function.”, 87);
log(“ERROR”, “Error at Network.js”, “Maximum callstack”, 22);
DEBUG, WARN ve INFO log türleri için de benzer şekilde çağrımlar yapılacaktır. Burada fark edebileceğiniz gibi ilk ve ikinci parametre sürekli tekrar ederek kodlanıyor. Bunu currying ile indirgeyebiliriz. Öncelikle curry metodumuz 3 adet parametre aldığı için, değişken sayıda parametre alabilecek şekilde aşağıdaki gibi değiştrelim:
const curry = (f) => {
return function curryliFonksiyon(…parametreler) {
if (parametreler.length < f.length) {
return function () {
return curryliFonksiyon.apply(null, parametreler.concat([].slice.call(arguments)));
};
}
return f.apply(null, parametreler);
};
};
Şimdi diğer log türleri için aşağıdaki gibi curried fonksiyonlar oluşturabiliriz.
let logError = curry(log)(“ERROR”)(“Error At Network.js”);
let logWarn = curry(log)(“WARN”)(“Warn At Network.js”);
let logInfo = curry(log)(“INFO”)(“Info At Network.js”);
Artık oluşturulan bu fonksiyonları kullanabiliriz:
logError(“Hata mesajı”, 21);
logWarn(“Uyarı mesajı”, 78);
logInfo(“Bilgi mesajı”, 12);
Konsoldaki çıktısı aşağıdaki gibi olacaktır:
Curried fonksiyonlar ile daha kısa ve yalın kod yazabildik. Soldan sağa tekrar eden kod çağrımları için currying’den faydalanabiliyoruz. Peki ya sağdan sola doğru tekrar eden çağrılarda ne yapacağız? Burada partial application (kısmî uygulama) imdadımıza koşuyor.
Partial application (Kısmî uygulama tekniği)
Currying’in işimize yaramayacağı şekilde sağdan sola doğru tekrar eden kısımların olduğu çağrımlar olabilir. Bu çağrımlar için parametrelerin bir kısmının (partial) geçirileceği partial
fonksiyonu kullanılır. Sağdan sola doğru tekrar eden çağrımlara örnek olarak setTimeout
fonksiyonunu verebiliriz:
setTimeout(() => console.log(“A planını uygula”), 3000);
setTimeout(() => console.log(“B planını uygula”), 3000);
Burada göreceğimiz gibi her setTimeout çağrısına süre olarak 3000
değerini geçiriyoruz. Currying işlemi soldan sağa doğru uygulandığı için burada partial fonksiyonuna ihtiyacımız var:
const partial = function (fMain, …rightArgs) {
return function (…fPlaceholders) {
rightArgs[0] = “placeholder”;
for (var i = 0, j = 0; i < rightArgs.length && j < fPlaceholders.length; i++) {
if (rightArgs[i] === “placeholder”) {
rightArgs[i] = fPlaceholders[j++];
}
}
return fMain.apply(null, rightArgs);
};
};
Fonksiyon çağrımları aşağıdaki hale gelecektir:
let ucSaniyeSonraYap = partial(setTimeout,“placeholder”,3000);
ucSaniyeSonraYap(() => console.log(“A planını uygula”));
ucSaniyeSonraYap(() => console.log(“B planını uygula”));
Composition’lar oluşturma ve Pipelining
Pure fonksiyonlardan kompozisyon oluşturarak, birbirinden bağımsız birimleri birbiri ile haberleşecek hale getirebiliriz. Bu sayede ayrı ayrı test edilebilir fonksiyonlar elde etmiş oluruz. Basit bir compose
fonksiyonu aşağıdaki gibidir:
const compose = (f, g) =>
(x) => f(g(x));
Burada aynı matematikte olduğu gibi öncelikle g(x) çalıştırılır ve sonucu f fonksiyonuna aktarılarak f fonksiyonu çalıştırılır. Böylelikle çalıştırma işlemi sağdan sola doğru akarak ilerler. compose
fonksiyonunu kullanmadan önce imperative bir kod ele alalım ve bunun compose karşılığını inceleyelim.
var sayi = parseFloat(“3.14”);
var pi = Math.round(sayi);
Burada pi sayısının değeri 3 olacaktır. Bunun yerine compose ile aynı işlemi daha az kodla gerçekleştirebiliriz:
const yuvarla = compose(Math.round,parseFloat);
yuvarla(“3.14”);
// << 3
Artık ürettiğimiz yuvarla
fonksiyonunu her yerde kullanabiliriz. Bir örnek daha yapalım. Aşağıdaki gibi 2 adet fonksiyonumuz bulunsun:
const kelimelereAyir = (cumle) => cumle.split(” “);
const say = (array) => array.length;
Bu fonksiyonları kullanarak cümledeki kelimeleri sayan fonksiyonu aşağıdaki gibi oluşturabilir ve kullanabiliriz:
const kelimeleriSay = compose(say,kelimelereAyir);
kelimeleriSay(“JavaScript ile fonksiyonel programlama çok eğlenceli.”);
//<< 6
İkiden fazla fonksiyon için compose fonksiyonunu aşağıdaki gibi refactor edebiliriz:
const compose = (…fonksiyonlar) => (deger) =>
fonksiyonlar.reverse().reduce((baslangicDegeri, f) => f(baslangicDegeri), deger);
Yeni compose fonksiyonunu test etmek için yeni bir fonksiyon daha oluşturalım ve önceki iki fonksiyon ile birlikte compose edelim:
const tekMiCiftMi = (sayi) => sayi % 2 == 0 ? “çift” : “tek”;
const kelimeleriSayTekMiCiftMi = compose(tekMiCiftMi, say, kelimelereAyir);
kelimeleriSayTekMiCiftMi(“JavaScript ile fonksiyonel programlama çok eğlenceli.”);
// << çift
Pipelining ise compose’un tam tersi olacak şekilde soldan sağa doğru çalışır. Oluşturulan pipe
fonksiyonunun compose
‘dan farkı reverse
edilmemesidir:
const pipe = (…fonksiyonlar) => (deger) =>
fonksiyonlar.reduce((baslangicDegeri, f) => f(baslangicDegeri), deger);
const kelimeleriSayTekMiCiftMi = pipe(kelimelereAyir, say, tekMiCiftMi);
kelimeleriSayTekMiCiftMi(“JavaScript ile fonksiyonel programlama çok eğlenceli.”);
// << çift
pipe’ın compose’a göre okunabilirliği daha fazladır. Bu sayede bir dizi komut yazar gibi arka arkaya fonksiyon isimlerini vererek iş yaptırabilirsiniz.
Functor’lar nedir?
Bir functor, map
adındaki bir fonksiyonu implement eden ve içerisinde herhangi bir değer tutan veri tipidir. Diğer bir deyişle, bir functor olabilmesi için map fonksiyonunun o veri tipinde barındırılması zorunludur. Bir örnekle inceleyelim:
const Container = val => ({
map: f => Container(f(val)),
value: val
});
Container’ımızı oluşturduğumuza göre içerisine değer atarak üzerinde işlemler gerçekleştirebiliriz:
const karesiniAl = (x) => x * x;
Container(2).map(karesiniAl);
// << {map: ƒ, value: 4}
Dönen değer aynı zamanda bir Container olduğu için, arka arkaya map fonksiyonunu çağırabiliriz:
Container(2)
.map(karesiniAl)
.map(karesiniAl)
.map(karesiniAl);
// << {map: ƒ, value: 256}
Günlük hayatta functor’lar, null kontrollü bir şekilde fonksiyonun çağrılması gibi farklı şekillerde kullanılabilir.
MayBe functor’ı ile otomatik null kontrolü
Container’dan farklı olarak, null
kontrolünü kendi içerisinde gerçekleştirecek olan MayBe functor’ını yazalım:
const MayBe = val => ({
value: val,
// Eğer aktarılan parametre null ise içerisinde null barındıran bir MayBe nesnesi geri döndürülür
map: fn => val == null ? MayBe(null) : MayBe(fn(val)),
// MayBe’den değer alınırken eğer null ise varsayilan bir değer geri döndürülmesini sağlayabiliriz
getOrElse: varsayilan => val == null ? varsayilan : val
});
Birkaç örnek üzerinde uygulayacak olursak:
var value = “JavaScript”;
MayBe(value).map(x => x.toUpperCase()).getOrElse(“Parametre bulunamadı”);
//<< “JAVASCRIPT”
value = null;
MayBe(value).map(x => x.toUpperCase()).getOrElse(“Parametre bulunamadı”);
//<< “Parametre bulunamadı”
null üzerinde işlem yapmamıza rağmen kodun patlamaması gerçekten güzel bir durum. Bu şekilde MayBe kullanılarak güvenilir bir kod yazılabilir.
Either functor’ı ile hata yakalama
Either functor’ında genellikle iki adet alt functor kullanılır: Left ve Right functor’lar. Başarılı durumlarda Right, hata alınan durumlarda Left functor çalıştırılır. Şimdi bu iki functor’ı oluşturalım:
const Left = value => ({
map: f => Left(value),
value
});
Left(5).map(v => v * 2).value // 5
Left functor’ı hata olması durumunda sadece ilgili hata bilgisini barındırması gerektiği için map fonksiyonu etkisiz işlem gibi davranır.
const Right = value => ({
map: f => Right(f(value)),
value
});
Right(5).map(v => v * 2).value // 10
Right functor’ı ise map’ten aldığı fonksiyona ilgili veriyi parametre olarak geçerek çalıştırır. Şimdi aşağıdaki gibi bölme işlemi yapan bir fonksiyonumuz olsun:
const bolmeIslemi = (bolunen, bolen) => {
if (typeof bolunen != “number” || typeof bolen != “number”) {
return Left(new Error(‘Bölünen ve bölen number tipinde olmalıdır.’));
}
if (bolen == 0) {
return Left(new Error(‘Bölen 0 olamaz’));
}
return Right(bolunen/bolen);
}
Bölme işlemi fonksiyonunu deneyelim:
bolmeIslemi(1,2).map(x => “Sonuç: “ + x).value
// << “Sonuç: 0.5″
bolmeIslemi(1,0).map(x => “Sonuç: “ + x).value
// << Error: Bölen 0 olamaz
// << at bolmeIslemi (<anonymous>:6:21)
// << at <anonymous>:1:1
Şu anda uygulama patlamadan map fonksiyonunun çalışmasını sağlayabiliyoruz. Fakat hata olması durumuna özel olarak hata logunun console’a basılması gibi işler gerçekleştiremiyoruz ve sadece Error nesnesini geri döndürebiliyoruz. Bunu iyileştirmek için Left ve Right functor’a birer catch fonksiyonu ekleyebiliriz:
// Left functor’ında hata alındığında ilgili değeri Right functor’ına iletecek
const Left = value => ({
map: f => Left(value),
catch: f => Right(f(value)),
value
});
Left(new Error(‘undefined is not an object’)).catch(error => error.message).value
// << “undefined is not an object”
// Right’ta ise catch’in hiçbir işlem yapmadan kendisini dönecek şekilde düzenleyelim
const Right = value => ({
map: f => Right(f(value)),
catch: () => Right(value),
value
});
Right(5).catch(error => error.message).value
// << 5
İki functor’ı birbiri ile efektif bir şekilde kullanmak için tryCatch fonksiyonu oluşturalım:
const tryCatch = f => (x, y) => {
try {
return Right(f(x, y));
} catch (error) {
return Left(error);
}
};
bolmeIslemi fonksiyonunu artık functor kullanmayacak şekilde düzenleyelim:
const bolmeIslemi = tryCatch((bolunen, bolen) => {
if (typeof bolunen != “number” || typeof bolen != “number”)
throw new Error(‘Bölünen ve bölen number tipinde olmalıdır.’);
if (bolen == 0)
throw new Error(‘Bölen 0 olamaz’);
return bolunen/bolen;
});
// Başarısız durumda
bolmeIslemi(1,0)
.map(x => “Sonuç: “ + x)
.catch(x => console.error(“Hata: “ + x["message"]))
.value; // “Hata: Bölen 0 olamaz”
// Başarılı durumda
bolmeIslemi(1,2)
.map(x => “Sonuç: “ + x)
.catch(x => console.error(“Hata: “ + x["message"]))
.value; // “Sonuç: 0.5″
Bu sayede hata olduğunda console’a hata mesajı bastırdık, normal durumda da sonucu hesaplayabildik.
Chain fonksiyonu ile map()’ten sonra bir fonksiyon daha çalıştırma
Chain fonksiyonu, map()
fonksiyonu çalıştıktan sonra varsayılan olarak yapılacak işler için kullanılır. Aslında chain()
‘i iki adet map fonksiyonunun arka arkaya çalıştırılması gibi düşünebilirsiniz. JavaScript’te yaygın olarak kullanılan flatMap() fonksiyonu da bir chain’dir ve map().flat()
şeklinde çalışır. Fakat her chain flatMap işlemi yapmak zorunda değildir. Chain fonksiyonu için örnek olarak bir sayı yuvarlama fonksiyonu oluşturalım:
// Chain fonksiyonu aldığı fonksiyonu map ettikten sonra,
// oluşan sayıyı yuvarlayacak şekilde değiştirelim:
const Right = value => ({
map: f => Right(f(value)),
catch: () => Right(value),
chain(f) {
return Right(Math.round(this.map(f).value));
},
value
});
Right’a chain eklediğimiz için hata olması durumunda karşılamak adına Left’e de eklememiz gerekiyor. Fakat buradaki chain metodu hiçbir şey yapmadan direkt dönecektir:
const Left = value => ({
map: f => Left(value),
catch: f => Right(f(value)),
chain: f => Left(value),
value
});
Şimdi aşağıdaki şekilde bölme işlemi sonucunda oluşacak yarıçaplı çemberin çevresini hesaplatıp oluşan sonucu yuvarlatabiliriz.
bolmeIslemi(2, 2)
.map(x => 2 * x)
.chain(x => x * Math.PI)
.catch(x => console.error(“Hata: “ + x["message"]))
.value;
// << 6
Monad nedir?
Monad kelime anlamı olarak atom, birim gibi en küçük yapıda olan nesnelere denir. Fonksiyonel programlamada ise, hataya yol açabilecek yan etkili işlemlerin (örn:null), Maybe gibi opsiyonel tipler ile sarmalanarak yan etkisiz hale getirilmesidir.
Sonuç Olarak
Fonksiyonel programlama sayesinde fonksiyonları değişken tanımlar gibi anlık tanımlayarak işlerimizi daha kolay hale getirecek küçük fonksiyonlar yazıp bunları birbiri ile çalışacak şekilde bağlayabiliyoruz. Eğer fonksiyonel programlama hakkında soru ve görüşleriniz varsa bize yorum bölümünden yazabilirsiniz. Bir sonraki yazıda diziler üzerinde işlem yapan map, filter, reduce gibi fonksiyonları ele alacağız. Görüşmek üzere…