Filter, Map ve Reduce İşlemleri


Filter, Map ve Reduce işlemleri ile performanslı, hızlı ve pratik kod geliştirme.


Bu işlemleri bir yemeğin hazırlanışına benzetirim hep. Daha akılda kalıcı oluyor böylelikle. Konuya hızlıca gireceğim, size melemen nasıl kodlanır onu anlatarak başlayayım.

Melemen yapmak için mutfağımızda ki bir çok malzemeyi belirli oranlarda kullanacağız. Peki hangi malzemeleri kullancağız işte bu işlemi arrayFilter fonksiyonu yapıyor yani elimizde ki tüm verinin içinden bize lazım olanları ayıklıyor. Demek ki melemen için kullanacağımız tuz, yağ, domates, biber ve yumurtayı bulmak için filter fonksiyonu gerekiyor. Filter edilmemiş bir veri istemediğimiz şeyleri içerebilir, aynı bizim mutfağımız gibi. Artık elimizde malzemelerimiz var. Peki bunları bir tencereye koyup pişmesini mi bekleyeceğiz? Hayır kesinlikle domatesleri ve biberleri doğramamız, yumurtayı kırmamız gerekiyor. Bunun için arrayMap fonksiyonunu kullanmamız gerekiyor. Map işlemi bir veriyi işleyerek başka bir türe veya sonuca çevirerek bize dönüştürmesini sağlar. Yani domateslerin doğranması gibi işlemler map fonksiyonuna aittir. Bunu veri gibi düşünürsek kdv oranı ile salt tutarın çarpımı sonucu bulacağımız kdv tutarı diyebiliriz. Aslında kdv tutarı bir değer değil bir sonuçtur. Veyahut satır net toplamı gibi satır içerisinde işlenmiş veriler map ile işlenmelidir. Evet artık malzemelerimizden işleme tabi tutulacakları işledik yani domatesleri biberleri kestik yumurtayı kırdık. Şimdi işin pişirme kısmına geldi. Reduce burada asıl dikkat edilmesi gereken konu. Çünkü reduce işlemi sonucu oluşan sonuç artık baştaki sonuçlardan herhangi birine ulaştırmaz. Yani melemeni pişirdiğinizde artık ona domates, biber, yumurta, yağ ve tuzun pişmiş hali demeyiz, direk melemen deriz. Reduce bir veri dizesi üzerinde tekil sonuç üretir. Örneğin fatura satırlarının toplamı diyebiliriz. Bazen gruplama işlemide yapabilir, örneğin kdv oranlarına göre toplamları. Burada gruplama sonucu birden fazla veri de oluşsa sonuç itibari ile ana veriye dönmek mümkün değil, yeni bir veri oluşmuş olmasıdır. Yemek tarifleri konusunda çok başarılı değilimdir, öyle olsam yemek kitabı çıkarırdım keza bu yazılımdan daha iyi bir iş olurdu :)

List ve Array ile başlayan fonksiyonlar için ön bilgilendirme

Aynı fonksiyon postfixine sahip list ve array prefixli fonksiyonlar görüyorsanız array ile başlayanları tercih edin. Örneğin arrayFilter ile listFilter fonksiyonları array ve list ler üzerinde filtreleme işlemi yaparlar. Ancak aslında list sanal bir dizidir. Her seferinde list diziye dönüştürülür işlem yapılır sonra tekrar liste dönüştürülür. Bu nedenle sonuç list olarak kulanılacak olsa bile işlemi array üzerinden yürütmek, başlangıç değerimiz list ise listToArray ile array a çevirmeli tüm işlemler tamamlandıktan sonra list e tekrar dönüştürülmelidir. Her defasında list lerin array a dönüştürülmesi ve tekrar liste dönüştürülmesini engellemek gerekir. Konumuz bu yüzden array prefixli fonksiyonlar üzerinden anlatılacaktır.

arrayFilter(array, function(element [,index [,array]])) array

Filter fonksiyonu dizi içerisinde ki tüm elemanları ikinci parametrede verilen fonksiyona teker teker göndererek işlem yapılmasını sağlar. Dizinin elemanları fonksiyona element değişkeni üzerinden, sıra numarası index üzerinde ve ana dizin array değişkeni üzerinden iletilir. Yazılacak fonksiyonda en azından element değişkeni belirtilmelidir, diğer değişkenlerin belirtilmesine gerek yoktur.

Genellikle bir dizi üzerinde ki elemanları filtreleyerek süzme işlemi görür. Örneğin müşteri adı "Halit" olan faturaların bulunması işlemini yapar. Bunun için faturalar adında bir diziden musteri alanı "Halit" olan kayıtları seçmek istersek şu şekilde kullanabiliriz.

<cfset halitin_faturalari = arrayFilter(faturalar, function(elm) { return elm.musteri eq "Halit"; })>

Dikkat edilmesi gereken nokta sonucun boolean olarak döndüğü tüm fonksiyonlar bu işlem için çalışabilir. Yani fonksiyon öbeğinde işlem yapılıp sonucun belirli bir değeri aşanları gibi daha kompleks işlemler içinde kullanılır. Ancak unutmamamız gereken nokta eğer filtre içerisinde hesaplanacak ve kontrol edilecek değer daha sonra kullanılabilecek bir durumda ise önce map layarak verilerin hesaplanıp daha sonra basit filtre seviyesine indirilmesi uygun olacaktır. Ama sadece hesap sonucu filtre yapılacak ise map yapmak ekstra veri üretmeniz anlamına gelebilir. Bu örnekte index ve ana diziyi tekrar kullanmadık dikkatinizi çekmek isterim.

arrayMap(array, function(element [,index [,array]])) array

Map fonksiyonu dizi içerisinde ki tüm elemanları ikinci parametrede verilen fonksiyona teker teker göndererek yeni bir değer oluşmasını sağlar. Dizinin elemanları fonksiyona element değişkeni üzerinden, sıra numarası index üzerinde ve ana dizin array değişkeni üzerinden iletilir. Yazılacak fonksiyonda en azından element değişkeni belirtilmelidir, diğer değişkenlerin belirtilmesine gerek yoktur. Burada oluşan yeni değer aynı index ile sonuç dizisine eklenir. Eklenecek eleman yeni bir değişken olabileceği gibi mevcut değişkenin modifiye edilmiş hali de olabilir.

Genellikle hesaplanarak sonuca ulaşılan işlemler için kullanılır. Daha önce verdiğim örnekte ki gibi salt satır toplamını kdv oranı ile çarparak kdv tutarını bulmak gibi işlemlerde kullanabiliriz. Örneğin bir dizide toplam ve kdv alanları olsun ve bu satıra kdv_tutari alanını hesaplayıp ekleyelim.

<cfset fatura_satirlar = arrayMap(fatura_satirlar, function(elm) { elm.kdv_tutari = elm.kdv * elm.toplam; return elm; })>

Bu örnekte fatura_satirlar dizisinde ki her bir element için kdv_tutari alanı hesaplanmış, var olan struct a eklenmiş ve yeni değer olarak var olan ve eklenmiş struct dönüştürülmüştür. Sonuç aynı değişkene aktarılmıştır, ki bu aynı değişkenin modifiye edilmesi (yani var olan değerlerin bozulmadığı) durumu olduğu için yapılmıştır. İşlemin bu şekilde yapılması makuldür. Ancak dizide ki elementleri eskisinden farklı hale getirecek bir işlem yapıyorsanız eski dizi değişkenine atamanız yanlış bir hareket olabilir. Mesela biz dizi de ki TL cinsinden değerleri USD cinsinden dönüştürüp yeni bir diziye alabilirsiniz.

arrayReduce(array, function(result, element [, index [, array]]) [,initial_value]) any

Reduce fonksiyonu dizi içerisinde ki her bir elementi fonksiyona göndererek, result değerine atayıp result değerini sonuç olarak döndürür. Bu işlem biraz kafa karıştırıcı olsa da aslında her satırın sonucunu result değerine ekleyip tekrar fonksiyonu son result değerini de vererek çağrıldığı bir sisteme benzetebiliriz. 

Genellikle alt toplam veya gruplayarak toplam gibi işlemlerde kullanılır. Örneğin bir faturanın toplamını ve kdv oranına göre kdv toplamlarını bulalım.

<cfset genel_toplam = arrayReduce(fatura_satirlar, function(sonuc, elm) { sonuc = sonuc?:0; sonuc = sonuc + elm.toplam; return sonuc; })>

<cfset kdv_toplami = arrayReduce(fatura_satirlar, function(sonuc, elm) { sonuc = sonuc?:structNew(); sonuc["kdv_#elm.kdv#"] = (structKeyExists(sonuc, "kdv_#elm.kdv#") ? sonuc["kdv_#elm.kdv#"] : 0) + elm.kdv_tutar; return sonuc; })>

Bu örnekte hem düz mantık toplam alınmış hem de gruplama mantığıyla toplam alınmıştır. Düz toplam da her defasında sonuç değerine ekleme yapılarak nihai sonuca ulaşılmışken, gruplama yapılarak yapılan işlem de ise geriye başka bir struct oluşturulup döndürülmüştür. Burada dikkat edilmesi gereken konu initial_value her zaman kararlı çalışmadığı için işlemlere başlamadan önce sonuc değerini başlangıç değeriyle set etmek olaraktır.

Pipe işlemler

Ardışık ve iç içe çalışarak sonuç üretmeye çalışan işlemler için bu kavram kullanılır. Örneğin fatura satırları içerisinde promosyon olmayan satırların genel kdv tutar toplamını alalım.

<cfset kdv_toplami = arrayReduce( arrayMap( arrayFilter( fatura_satirlar, function(elm) { return elm.is_promotion eq 0; } ), function(elm) { elm.kdv_tutari = elm.kdv * elm.toplam; return elm; } ), function(result, elm) { result = result?:0; result = result + elm.kdv_tutari; return result; } )>

İç içe ihtiyaca binaen filter map ve reduce işlemini sonsuz kez çağırabilirsiniz, filtreleyip hesaplayıp hesaplanan alana göre yeniden filtreleyip, gruplanmış sonuç oluşturup, hesaplayıp bu sonucu da filtreleyip tekrar sonuca çevirebilirsiniz. Yani iç içe geçmiş bu üçlü ile hızlı bir şekilde kodları istediğiniz hale getirebilirsiniz. Tabi üçünü kullanmak zorunda değilsiniz, filter ve map gibi sadece ikili bir süreç te ihtiyacınızı karşılayabilir veya map ve reduce gibi hiyeraşik verileri hesaplaya hesaplaya üst seviyelere çekebilirsiniz.

Uygun yaklaşım

Efendim, biz bu işlemleri örneğin tablolar içerisinde loop operasyonları yaparken yani alanları ekrana basarken de hesaplayabiliyoruz diyebilirsiniz. Ancak mühim nokta şu model ve view i birbirinden ayırmak.

Bu konu başlığında verdiğim üç mühim fonksiyon ve vermediğim diğer fonksiyonların amacı modeli şekillendirmektir. Yani model üzerinde yapacağınız pipe işlemler ile topluluk babında geliştirilen yazılımlarda yaptığınız hesaplamalar ve sonuçların ne olduğunu diğer yazılımcılar model seviyesinde olacağı için net bir şekilde anlayacak html ve diğer kodların arasında saklanmış aggregate işlemlerin olmadığını bileceklerdir. İşlemi output yapmadan önce bitirebiliyorsanız yeni bir özellik eklendiğinde ya pipe üzerine bir node daha açmış olursunuz yada yeni bir değişkene gerekli değerleri atarsınız. Spagetti olarak tabir edilen geleneksel loop sürecinde ki işlemler anlaşılması zor, gözden kaçırılan hesaplamalarla dolu, yada aralara girmenin riskli ve zorlu olduğu kodlar ortaya çıkar. Clean code yaklaşımı model ve view kodlarının aynı dosyada dahi olsa birbirinden yalıtılmış mümkün mertebe gruplanmış olarak bulunmasını ön görür. Ayrıca bu fonksiyonlar yine kodu minimize ederek spagetti sürecinde oluşacak ara değişkenlerin kalabalığını ortadan kaldırır. Query of query kavramında bazı kısıtlamaları aşabilen ve query of query ile iç içe çalışabilen bu fonksiyonları daha sonra yayınlayacağım yazımda detaylarıyla anlatacağım.


Yazan: Halit Yurttaş - Workcube Software Architecture


Geri Bildirim

Bu içeriği faydalı buldunuz mu?
İlişkili İçerikler