“По-функционален” Swift — втора част — списъци

Radoslav Radenkov
paysafe-bulgaria
Published in
5 min readNov 9, 2018

В първия пост показахме някои принципи от функционалното програмиране и как могат да ни бъдат полезни. В този ще подходим по-практично и ще се фокусираме как можем да ги приложим при работата със списъци и масиви. Стандартната библиотека в 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) -> Int
let 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 и начините, по които да ги ползваме.

--

--