High Order Functions Principles

Furkan Eruçar
MobvenLab TR
5 min readAug 24, 2023

--

Bu yazımda sizlere swiftte yüksek dereceden fonksiyonların işleyişlerini göstereceğim.

İlk başta zor gözükse de kullandıktan sonra alışması kolay Yüksek dereceden fonksiyonlarla uğraşırken dikkat edilmesi gereken hususlardan biri Complexitydir.

“Complexity” (karmaşıklık), bir algoritmanın veya bir programın çalışma süresini veya kaynak kullanımını değerlendiren bir terimdir. Genellikle veri setinin boyutu veya girdilerin sayısı arttıkça algoritmanın veya programın performansının nasıl değiştiğini anlamak için kullanılır.

Karmaşıklık, iki temel türde ölçülür:

  1. Zaman Karmaşıklığı (Time Complexity): Bir algoritmanın işlem süresini girdi boyutuna bağlı olarak değerlendirir. Genellikle Big O (O büyük) notaasyonuyla ifade edilir. Örneğin, O(n) zaman karmaşıklığı, girdi boyutu n arttıkça işlem süresinin doğru orantılı olarak artacağını belirtir.
  2. Alan Karmaşıklığı (Space Complexity): Bir algoritmanın ne kadar hafıza (bellek) kullandığını girdi boyutuna bağlı olarak değerlendirir. Benzer şekilde Big O notaasyonuyla ifade edilir. Örneğin, O(n) alan karmaşıklığı, girdi boyutu n arttıkça kullanılan bellek miktarının doğru orantılı olarak artacağını belirtir.

Karmaşıklık analizi, farklı algoritmaların veya yaklaşımların veri seti büyüdükçe nasıl performans gösterdiğini karşılaştırmak ve daha verimli algoritmaları seçmek için kullanılır. Daha düşük zaman veya alan karmaşıklığı, daha iyi performans ve daha verimli kaynak kullanımı anlamına gelir.

Complexity hesaplanırken eğer yüksek dereceden fonksiyonlar iç içe kullanılıyorsa çarpılır aksi halde toplanır

func sumOfArray(_ array: [Int]) -> Int {
var sum = 0
for _ in 0..<array.count {
for number in array {
sum += number
}
}
return sum
}

let numbers = [1, 2, 3, 4, 5]
let totalSum = sumOfArray(numbers) //

Yukarıdaki örnekte iç içe kullanılan for döngülerinden dolayı complexity

O(n) . O(n) = O(n²)

olarak hesaplanır. Benzer şekilde iç içe kullanılmayan ve complexity’si O(n) olan iki yüksek dereceden fonksiyonun complexity’si ise

O(n) + O(n) = O(2n)

olarak hesaplanır. Ancak büyük O gösterimi içinde sabit terimler genellikle ihmal edilebildiği için O(2n) yerine sadece O(n) olarak da ifade edilebilir.

Aşağıda Bahsedeceğim yüksek dereceden fonksiyonların complexityleri ise şu şekildedir:

.filter O(n)

.sort O(n logn)

.map O(n)

.forEach O(n)

for loop O(n)

.filter

Bir koleksiyondaki elemanları belirli bir koşula göre filtrelemek için kullanılan yüksek seviyeli bir fonksiyondur. Bu fonksiyon, genellikle diziler veya koleksiyonlar üzerinde çalışırken, belirli bir koşulu sağlayan elemanları seçmek için kullanılır ve sonuç olarak yeni bir koleksiyon döndürür..filter fonksiyonun çalışma prensibi şu şekildedir:

  1. Önceki koleksiyonun her bir elemanı üzerinde belirtilen koşul kontrol edilir.
  2. Eğer eleman, belirtilen koşulu sağlarsa, o eleman yeni bir koleksiyona eklenir.
  3. İşlem sonunda, koşulu sağlayan elemanların bulunduğu yeni bir koleksiyon döndürülür.
let numbers = [1, -2, 3, -4, 5, -6, 7, -8] 
let positiveNumbers = numbers.filter { $0 > 0 }
print(positiveNumbers) // Output: [1, 3, 5, 7]

fonksiyonunun arkasındaki işleyiş ise, koleksiyon üzerinde döngüler kullanarak ve belirli bir koşulu kontrol ederek gerçekleştirilir.

extension Array {
func customFilter(_ isIncluded: (Element) -> Bool) -> [Element] {
var result: [Element] = []
for element in self {
if isIncluded(element) {
result.append(element)
}
}
return result
}
}

let numbers = [1, -2, 3, -4, 5, -6, 7, -8]
let positiveNumbers = numbers.customFilter { $0 > 0 }
print(positiveNumbers) // Output: [1, 3, 5, 7]

customFilter fonksiyonu, Swift’in standart kütüphanesindeki .filter fonksiyonunun arkasındaki temel prensibi gösterir ve benzer bir şekilde çalışır, ancak orijinal fonksiyon daha optimize ve genel kullanıma uygun bir şekilde implement edilmiştir.

.sort

Bir koleksiyonun elemanlarını belirli bir sıralama düzenine göre yeniden düzenlemek için kullanılan bir fonksiyondur. Örneğin, bir dizideki sayıları küçükten büyüğe veya büyükten küçüğe sıralamak için .sort fonksiyonu kullanılabilir..sort fonksiyonunun işleyişi genel olarak şu adımları içerir:

  1. Koleksiyonun elemanları üzerinde döngü başlatılır.
  2. Her elemanın, belirli bir sıralama kriterine göre karşılaştırılabilir bir değeri bulunur.
  3. Bu karşılaştırılabilir değerlere göre elemanlar sıralama işlemi gerçekleştirilir. Örneğin, sayılar için küçükten büyüğe sıralama yapılacaksa, sayıların değeri kullanılır.
  4. Koleksiyonun elemanları, belirtilen sıralama düzenine göre yeniden düzenlenir ve sıralanmış hali döndürülür.
var numbers = [5, 2, 9, 1, 5, 6]
numbers.sort(by: { $0 > $1 })
print(numbers) // Output: [1, 2, 5, 5, 6, 9]

Örnek olarak, Swift dilindeki .sort fonksiyonunun temel işleyişini aşağıdaki gibi düşünebiliriz:

extension Array where Element: Comparable {
mutating func customSort() {
for i in 0..<self.count {
for j in i+1..<self.count {
if self[i] > self[j] {
self.swapAt(i, j)
}
}
}
}
}

var numbers = [5, 2, 9, 1, 5, 6]
numbers.customSort()
print(numbers) // Output: [1, 2, 5, 5, 6, 9]

customSort fonksiyonu, Swift’in standart kütüphanesindeki .sort fonksiyonunun arkasındaki temel prensibi gösterir ve benzer bir şekilde çalışır, ancak orijinal fonksiyon daha optimize, genel kullanıma uygun ve farklı sıralama algoritmaları içerebilecek şekilde implement edilmiştir.

.map

Bir koleksiyonun elemanlarını dönüştürmek veya işlemek için kullanılan yüksek seviyeli bir fonksiyondur. Bu fonksiyon, koleksiyonun her bir elemanını alır, belirli bir dönüşüm veya işlemi uygular ve sonuçları yeni bir koleksiyon olarak döndürür.

.map fonksiyonunun işleyişi genel olarak şu adımları içerir:

  1. Koleksiyonun elemanları üzerinde döngü başlatılır.
  2. Her eleman üzerinde belirli bir dönüşüm veya işlem uygulanır.
  3. Bu dönüşüm veya işlem sonucunda elde edilen değerler, yeni bir koleksiyonun elemanları olarak eklenir.
  4. Yeni koleksiyon oluşturulup dönüştürülmüş elemanlar eklenerek döndürülür.
let numbers = [1, 2, 3, 4, 5]
let squaredNumbers = numbers.map { $0 * $0 }
print(squaredNumbers) // Output: [1, 4, 9, 16, 25]

Örnek olarak, Swift dilindeki .map fonksiyonunun temel işleyişini aşağıdaki gibi düşünebiliriz:

extension Array {
func customMap<T>(_ transform: (Element) -> T) -> [T] {
var result: [T] = []
for element in self {
result.append(transform(element))
}
return result
}
}

let numbers = [1, 2, 3, 4, 5]
let squaredNumbers = numbers.customMap { $0 * $0 }
print(squaredNumbers) // Output: [1, 4, 9, 16, 25]

.forEach

Koleksiyonlar (dizi, set, sözlük gibi) üzerinde işlem yapmak için kullanılır. Bu fonksiyon, koleksiyonun her bir elemanı üzerinde belirli bir işlemi gerçekleştirmenizi sağlar..forEach fonksiyonunun çalışma mantığı şu şekildedir:

  1. İlk olarak, her elemanı sırayla dolaşır.
  2. Belirttiğiniz işlevi her eleman üzerinde çağırır.
  3. İşlevin dönüş değeri yoktur, yani elemanlarda herhangi bir değişiklik yapmak veya yeni bir koleksiyon oluşturmak gibi bir amaç taşımaz.
let numbers = [1, 2, 3, 4, 5]

numbers.forEach { number in
print($0)
}
// Output:
// 1
// 2
// 3
// 4
// 5

Örnek olarak, Swift dilindeki .forEach fonksiyonunun temel işleyişini aşağıdaki gibi düşünebiliriz:

extension Array {
func customForEach(_ body: (Element) -> Void) {
for element in self {
body(element)
}
}
}

let numbers = [1, 2, 3, 4, 5]
numbers.customForEach { print($0) }
// Output:
// 1
// 2
// 3
// 4
// 5

for Döngüsü

Yukarıdaki yüksek dereceden fonksiyonlarda gördüğümüz gibi aslında hepsinin temelinde bir for loop yatmaktadır. Peki ya temel olarak bu loop döngüsü nasıl oluşur?

  1. Başlangıç Değeri Belirleme: Döngü başlamadan önce bir başlangıç değeri belirlenir. Bu değer genellikle bir sayı veya bir koleksiyonun ilk elemanıdır.
  2. Koşul Kontrolü: Döngü her tekrarda bir koşul kontrol eder. Eğer koşul geçerliyse döngü devam eder, aksi takdirde sonlanır. Koşulun geçerli olup olmadığı, genellikle sayaç değişkenin veya koleksiyonun son değerine ulaşıp ulaşmadığına bağlıdır.
  3. İşlem Yapma: Her tekrarda döngü içinde belirtilen işlemler gerçekleştirilir. Bu işlemler, genellikle döngü içinde yer alan kod bloğunda yer alır.
  4. Sayaç Değişimi: Her tekrarda döngü içinde belirli bir sayaç değişkeni güncellenir. Bu değişiklik, döngünün hangi aşamasında olduğunu ve ne zaman sonlanması gerektiğini belirlemeye yardımcı olur.
  5. Tekrarlama veya Sonlandırma: Koşul hala geçerliyse, döngü başa döner ve adımlar 2–4 tekrarlanır. Koşul geçerli değilse, döngü sonlanır ve kontrol döngü sonrası kod satırlarına geçer.
let numbers = [1, 2, 3, 4, 5]

for number in numbers {
print(number * 2) // Output: 2, 4, 6, 8, 10
}

For döngüsü’nün temel işleyişini aşağıdaki gibi düşünebiliriz:

extension Array {
func customForLoop(_ body: (Element) -> Void) {
var index = 0
while index < count {
let element = self[index]
body(element)
index += 1
}
}
}

let numbers = [1, 2, 3, 4, 5]
numbers.customForLoop { number in
print(number * 2) // Output: 2, 4, 6, 8, 10
}

--

--