C# İle Fonksiyonel Programlama - Map

Giriş

Önceki yazımızda fonksiyon delegelerinden bahsettik. Bu yazımızdan itibaren fonksiyonel programlama yöntemlerini C# ile uygulama yöntemlerinden bahsetmeye başlayacağız.

  • Map
  • Filter
  • Reduce

Map

Steven alttaki tweetiyle Map, Filter ve Reduce yöntemlerini çok güzel şekilde ifade etmiş:

  • Cook ismindeki fonksiyonumuz Girdi olarak [🌽, 🐮, 🐔] alıyor ve herbirini pişirerek [🍿, 🍔, 🍳]‘e dönüştürüyor.
  • isVegeterian filtresi [🍿, 🍔, 🍳] arasından vejeteryanler için olmayanları filtreliyor.
  • eat reduce fonksiyonu ise [🍿, 🍳] alıp …

Biraz Teori

Hafifçe teorik gidelim:

Elimizde bir f(x) fonksiyonu olsun.

1f(x) = y

x, sıcaklık birimi Celcius olsun ve f(x) fonksiyonu, Celcius birimini Fahrenheit birimine çeviren bir fonksiyon olduğunda f(x) bir Map (dönüşüm) fonksiyonu diyebiliriz.

1f(°C) = °F

C# Map Kullanımı

Map fonksiyonunun C# dilindeki karşılığı IEnumerable extension metodu olan Select fonksiyonudur. Bu yazımızda Select fonksiyonunu kullanmayacağız, benzerini kendimiz geliştireceğiz.

1IEnumerable<TOut> map<T, TOut>(IEnumerable<T> source, Func<T, TOut> func) {
2  foreach (var element in source)
3    yield return func(element);
4}

Celcius ↔ Fahrenheit Dönüşüm Fonksiyonu

Celcius ↔ Fahrenheit dönüşüm fonksiyonundan yola çıkarak elimizdeki fonksiyonun bir adet girdisi ve bir adet çıktısı olmalı. Alttaki gibi dönüşüm fonksiyonumuz olduğunu farz edelim.

1Fahrenheit convertCtoF (Celcius value) => value * 1.8 + 32.0;

C# Delegelerini Kullanarak Kendi Map Fonksiyonumuzu Yazalım

Önceki yazılarımızda Func<T, TResult> delegesinden bahsetmiştik. Bu dönüşüm fonksiyonumuzu delege ile temsil etmek istersek:

1Func<Celcius, Fahrenheit> convertor = convertCtoF;

Elimizdeki bir Celcius veri setini, Fahrenheit değerlerine çevirecek bir fonksiyon yazalım ve bunu delegeleri kullanarak yapalım.

Alternatif #1

1Fahrenheit convertCtoF (Celcius value) => value * 1.8 + 32.0;
2
3IEnumerable<Fahrenheit> fahrenheit(IEnumerable<Celcius> values) {
4  foreach(var celcius in values) {
5    yield return convertCtoF(celcius);
6    //           ^^^^^^^^^^^
7  }
8}

Buradaki yaklaşım fonksiyonel programlama yöntemlerine uygun olmadı:

  1. fahrenheit fonksiyonu, parametreleri dışında global olan dış bir değişkene (convertCtoF) bağımlı. saf (pure) değil çünkü içeride kullanılan değerler parametrelerden alınmamış. Diğer ifadeyle hardcoded bir değer.

  2. convert fonksiyonuna direkt bağımlılık mevcut. Diğer ifadeyle tightly coupled convertCtoF fonksiyonu buraya parametre olara geçilebilmeliydi.

Çözüm, fonksiyonları delege olarak tanımlayarak low-level convertCtoF fonksiyonumuzu high level fahrenheit fonksiyonumuza parametre olarak geçmek.

Alternatif #2

1IEnumerable<Fahrenheit> fahrenheit(IEnumerable<Celcius> values, Func<Celcius, Fahrenheit> convertor) {
2  foreach(var celcius in values)
3    yield return convertor(celcius);
4}

Yeni fonksiyonumuza Func<Celcius, Fahrenheit> delege tipinde yeni bir parametre ekledik.Böylece bu delege tipine uyumlu herhangi bir fonksiyonu parametre olarak geçebileceğiz.

Kullanımı:

1Func<Celcius, Fahrenheit> convertor = convertCtoF;
2var fahrenheitValues = fahrenheit(celciusValues, convertor);

Genel amaçlı bir map fonksiyonu geliştirelim

Genel amaçlı bir map fonksiyonunu geliştirebilmek için elimizdeki hardcoded olan tip parametrelerini jenerik hale getirmemiz gerekli.

1IEnumerable<TResult> map<T, TResult>(IEnumerable<T> values, Func<T, TResult> convertor) {
2  foreach(var value in values)
3    yield return convertor(value);
4}

Kullanımı:

1Func<Celcius, Fahrenheit> convertor = convertCtoF;
2var fahrenheitValues = map<Celcius, Fahrenheit>(celciusValues, convertor);
3
4// Type parametreleri olmadan da çağırabiliriz.
5var fahrenheitValues = map<Celcius, Fahrenheit>(celciusValues, convertor);

Yeni map fonksiyonumuzla beraber sıcaklık değer listelerini, diğer birimlere çevirecek fonksiyonları tek tek yazmak yerine map<T, TResult> fonksiyonunu kullanarak türetebileceğiz.

Örnek:

1var celciusValues = new List<Celcius>() { -40, 0, 100 };
2
3var celciusToFahrenheitValues = map (celciusValues, convertCtoF);
4
5var celciusToKelvinValues = map<Celcius, Kelvin> (new List<Celcius>() { -273, 0, 100 }, value => value + 273.0);
6
7var fahrenheitToCelciusValues = map<Fahrenheit, Celcius> (celciusToFahrenheitValues, value => (value - 32) / 1.8);

Sonuç

Bu yazımızda fonksiyon delegelerini kullanarak genel amaçlı bir map implementasyonu yazmaya çalıştık. Siz de bir code kata 1 uygulaması olarak ÖTV ve KDV hesaplayan fonksiyonlar yazabilir, bu fonksiyonları delege yardımıyla map fonksiyonu aracılığıyla kullanabilirsiniz.

Örnek bir uygulama içeren LINQPad dosyasını ekte bulabilirsiniz.

Edit

  • 2020/12/20 - İş arkadaşım Zişan, LINQ Pad dosyası örneği yerine dotnet fiddle önerdi. Linki aşağıya bırakıyorum, kaydırarak açabilirsiniz.

Bağlantılar

  1. https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/select-clause
  2. https://docs.microsoft.com/en-us/dotnet/api/system.linq.enumerable.select?view=netcore-3.1
  3. MAP - dotnet fiddle