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

В първия пост показахме някои принципи от функционалното програмиране и как могат да ни бъдат полезни. В този ще подходим по-практично и ще се фокусираме как можем да ги приложим при работата със списъци и масиви. Стандартната библиотека в Swift съдържа трите основни функции за боравене с тях, които показахме и в примера, с който завършихме предната част:

  • Филтриране на елементите от списък отговарящи на желано условие — filter
  • Трансформиране (изобразяване) на списък чрез прилагане на една и съща операция към всеки елемент в нов списък — map
  • Акумулиране (комбиниране) елементите на списък — reduce

Трансформиране — map

Нека вземем масив от цели числа [1, 2, 3, 4, 5], за който искаме да умножим всяко от тях с 2 за да получим [2, 4, 6, 8, 10]. Едно примерно решение на задачата:

За нещо толкова просто имаме 3 реда програмен код, в които създаваме нов празен масив, итерираме елементите на масива, умножаваме всеки по 2 и го добавяме в новия масив. Същността на задачата — умноженето на всеки елемент по 2 — е скрита между шаблонния (boilerplate) код за създаването и итерирането на масива. Друг проблем в нашето решение е добавянето на елемените един по един. Това е операция, която е скъпа, защото се налага да се заделя нова памет за масива докато той расте. За да спестим това може предварително да резервираме необходимото количество елементи сmultipliedNumbers.reserveCapacity(numbers.count). Това добавя още един ред към нашата проста програма и също така е нещо, което рискуваме да забравим следващия път, когато пак ни се наложи да трансформираме един масив в друг. Стандартната библиотека в Swift ни предоставя лесен начин да се справим с посочените проблеми посредствум функцията map, която е дефинирана за протокола Sequence (списък).

func map<T>(_ transform: (Element) -> T) -> [T]

Създава се нов списък от елементи, всеки от които е резултат от изпълнението на функцията, подадена като аргумент, над един елемент от първия списък.

Използвайки функцията от нашата програма остава само същественото — умножението на числата, а за всичко останало оставяме да се грижи стандартната библиотека. Ако използваме съкратеният синтаксис, при подаване на функция като аргумент в Swift можем да го напишем дори на един ред numbers.map({ $0 * 2 }), където $0 означава първият аргумент на фунцията, която подаваме на map.

Филтриране — filter

Ще използваме същия пример с масив от цели числа [1, 2, 3, 4, 5], но този път искаме само четните от тях. Можем да я решим по следния начин:

Стандартната библиотека на Swift има функция filter, дефинирана за протокола Sequence (списък), която върши това.

func filter(_ isIncluded: (Element) -> Bool) -> [Element]

Съдава се нов списък от елементи в същия ред, за които фукцията, подадена като аргумент, връща true. Използвайки filter решението на нашата задача може да се напише на един ред: numbers.filter({ $0 % 2 == 0 })

Акумулиране/комбиниране — reduce

Отново се връщаме към примера с масива от цели числа [1, 2, 3, 4, 5]. Задачата ни този път е да сумираме елементите. Примерно решение изглежда по този начин:

В нашият прост пример има само една локална променлива и ясно се вижда къде я променяме, но ако си представим по-сложна функция броят на локалните променливи нараства. А това увеличава риска от грешки, които понякога са трудни за намиране. За да избегнем това можем да използваме reduce от стандартната библиотека на Swift дефинирана за протоколаSequence (списък):

reduce<Result>(_ initialResult: Result, _ nextPartialResult: (Result, Element) -> Result) -> Result

Връща се резултатa от комбинирането на елементите на списък използвайки акумулираща функция, подадена като аргумент, и дадена начална стойност. Акумулиращата функция приема два аргумента, временният резултат и текущият елемент и трябва да върне резултата от комбинирането им, който ще бъде подаден в следващата стъпка като аргумент. Началната стойност ще бъде използвана като временен резултат в първата стъпка.

Като използваме reduce, можем да решим задачата така:

Ако добавим и друга “супер-сила” на Swift — това че операторите са функции — получаваме решение на един ред: numbers.reduce(0, +)

Във всичките примери използвахме списъци с числа, но map, filter и reduce могат да се ползват върху масиви или списъци от всякакви обекти или структури.

След като се запознахме с map, filter и reduce нека се опитаме да ги използваме, вместо да пишем сложни функции, с които да трансформираме или филтрираме нашите данните. Така ще спазим принципа на модулността (композицията) от функционалното програмиране, да разбиваме задачите на по-малки части, за които имаме готово решение и което знаем, че работи. По този начин по-лесно ще откриваме евентуални проблеми и ще проследим какво се случва на всяка една стъпка. Също така имаме и лекотата бързо да променим нещо ако се наложи, например като променим само една от стъпките.

В следващият пост ще продължим с по-специфичните варианти на map — flatMap и compactMap и начините, по които да ги ползваме.

paysafe-bulgaria

Anything technical @ Paysafe Bulgaria

Radoslav Radenkov

Written by

iOS Developer @ Paysafe, Swift enthusiast

paysafe-bulgaria

Anything technical @ Paysafe Bulgaria