“По-функционален” Swift — втора част — списъци
В първия пост показахме някои принципи от функционалното програмиране и как могат да ни бъдат полезни. В този ще подходим по-практично и ще се фокусираме как можем да ги приложим при работата със списъци и масиви. Стандартната библиотека в Swift съдържа трите основни функции за боравене с тях, които показахме и в примера, с който завършихме предната част:
let numbers = [4, -5, 16, 9, 0, -9]
let sum = numbers.filter({ $0 > 0 }) // избираме само положителните числа
.map({ sqrt($0) }) // пресмятаме квадратните им корени
.reduce(0, +) // сумираме ги
- Филтриране на елементите от списък отговарящи на желано условие — filter
- Трансформиране (изобразяване) на списък чрез прилагане на една и съща операция към всеки елемент в нов списък — map
- Акумулиране (комбиниране) елементите на списък — reduce
Трансформиране — map
Нека вземем масив от цели числа [1, 2, 3, 4, 5]
, за който искаме да умножим всяко от тях с 2 за да получим [2, 4, 6, 8, 10]
. Едно примерно решение на задачата:
func multiplyBy2(numbers: [Int]) -> [Int] {
var multipliedNumbers = [Int]()
for number in numbers {
multipliedNumbers.append(number * 2)
}
return multipliedNumbers
}
За нещо толкова просто имаме 3 реда програмен код, в които създаваме нов празен масив, итерираме елементите на масива, умножаваме всеки по 2 и го добавяме в новия масив. Същността на задачата — умноженето на всеки елемент по 2 — е скрита между шаблонния (boilerplate) код за създаването и итерирането на масива. Друг проблем в нашето решение е добавянето на елемените един по един. Това е операция, която е скъпа, защото се налага да се заделя нова памет за масива докато той расте. За да спестим това може предварително да резервираме необходимото количество елементи сmultipliedNumbers.reserveCapacity(numbers.count)
. Това добавя още един ред към нашата проста програма и също така е нещо, което рискуваме да забравим следващия път, когато пак ни се наложи да трансформираме един масив в друг. Стандартната библиотека в Swift ни предоставя лесен начин да се справим с посочените проблеми посредствум функцията map, която е дефинирана за протокола Sequence (списък).
func map<T>(_ transform: (Element) -> T) -> [T]
Създава се нов списък от елементи, всеки от които е резултат от изпълнението на функцията, подадена като аргумент, над един елемент от първия списък.
numbers.map({ (number) in
return number * 2
})
Използвайки функцията от нашата програма остава само същественото — умножението на числата, а за всичко останало оставяме да се грижи стандартната библиотека. Ако използваме съкратеният синтаксис, при подаване на функция като аргумент в Swift можем да го напишем дори на един ред numbers.map({ $0 * 2 })
, където $0 означава първият аргумент на фунцията, която подаваме на map.
Филтриране — filter
Ще използваме същия пример с масив от цели числа [1, 2, 3, 4, 5]
, но този път искаме само четните от тях. Можем да я решим по следния начин:
func getEven(numbers: [Int]) -> [Int] {
var evenNumbers = [Int]()
for number in numbers where number % 2 == 0 {
evenNumbers.append(number)
} return evenNumbers
}
Стандартната библиотека на Swift има функция filter, дефинирана за протокола Sequence (списък), която върши това.
func filter(_ isIncluded: (Element) -> Bool) -> [Element]
Съдава се нов списък от елементи в същия ред, за които фукцията, подадена като аргумент, връща true. Използвайки filter решението на нашата задача може да се напише на един ред: numbers.filter({ $0 % 2 == 0 })
Акумулиране/комбиниране — reduce
Отново се връщаме към примера с масива от цели числа [1, 2, 3, 4, 5]
. Задачата ни този път е да сумираме елементите. Примерно решение изглежда по този начин:
func sum(numbers: [Int]) -> Int {
var sum = 0
for number in numbers {
sum = sum + number
}
return sum
}
В нашият прост пример има само една локална променлива и ясно се вижда къде я променяме, но ако си представим по-сложна функция броят на локалните променливи нараства. А това увеличава риска от грешки, които понякога са трудни за намиране. За да избегнем това можем да използваме reduce от стандартната библиотека на Swift дефинирана за протоколаSequence (списък):
reduce<Result>(_ initialResult: Result, _ nextPartialResult: (Result, Element) -> Result) -> Result
Връща се резултатa от комбинирането на елементите на списък използвайки акумулираща функция, подадена като аргумент, и дадена начална стойност. Акумулиращата функция приема два аргумента, временният резултат и текущият елемент и трябва да върне резултата от комбинирането им, който ще бъде подаден в следващата стъпка като аргумент. Началната стойност ще бъде използвана като временен резултат в първата стъпка.
Като използваме reduce, можем да решим задачата така:
numbers.reduce(0, { (sum, number)
return sum + number
})
Ако добавим и друга “супер-сила” на Swift — това че операторите са функции — получаваме решение на един ред: numbers.reduce(0, +)
Във всичките примери използвахме списъци с числа, но map, filter и reduce могат да се ползват върху масиви или списъци от всякакви обекти или структури.
// minimum element
numbers.reduce(Int.max, { (result, element) in
return element < result ? element : result
})// number of repeating elements
let fruits = ["banana", "cherry", "orange", "apple", "cherry", "orange", "apple", "banana", "cherry", "orange", "fig" ]let counts = fruits.reduce(into: [String: Int]()) { (counts, fruit) in
if let c = counts[fruit] {
counts[fruit] = c + 1
} else {
counts[fruit] = 1
}
}// chain operations
typealias IntTransform = (Int) -> Intlet multiplyBy2: IntTransform = { (arg) in
return arg * 2
}let minus5: IntTransform = { (arg) in
return arg - 5
}let operators: [IntTransform] = [multiplyBy2, minus5]
let result = operators.reduce(20) { (currentResult, transform) -> Int in
return transform(currentResult)
}
След като се запознахме с map, filter и reduce нека се опитаме да ги използваме, вместо да пишем сложни функции, с които да трансформираме или филтрираме нашите данните. Така ще спазим принципа на модулността (композицията) от функционалното програмиране, да разбиваме задачите на по-малки части, за които имаме готово решение и което знаем, че работи. По този начин по-лесно ще откриваме евентуални проблеми и ще проследим какво се случва на всяка една стъпка. Също така имаме и лекотата бързо да променим нещо ако се наложи, например като променим само една от стъпките.
В следващият пост ще продължим с по-специфичните варианти на map — flatMap и compactMap и начините, по които да ги ползваме.