CodeGym /Kurslar /Swift SELF /reduce(into:) — lüğətin yığılması

reduce(into:) — lüğətin yığılması

Swift SELF
Səviyyə, Dərs
Mövcuddur

1. Bəzən reduce(into:) lazım olur

İlk dəfə reduce ilə tanış olanda o, supergüc kimi görünür: “çoxlu element götürürük, bir dəyər alırıq”. Bu doğrudur — amma tam o vaxta qədər ki, “bir dəyər” massiv və ya lüğət kimi bir şeyə çevrilir. Məhz bu anda başlanğıc səviyyəli proqramçının beyni hər addımda “yeni massiv qaytarmağa” ciddi-cəhdlə çalışır, kod isə birdən-birə ya canavara, ya da özünü tez-tez köçürən yavaş bir konstruksiyaya çevrilir; sanki siz Xcode-u yenilədiyinizdən də tez.

Buradakı əsas problem Swift-in “pis” olmasında deyil, alətdən təyinatına uyğun istifadə etməməyimizdədir. Adi reduce hesablamalar üçün idealdır, o hallarda ki, akkumulyator rəqəm, sətir və ya məntiqi dəyər olur. Amma akkumulyator konteyner olduqda — massiv və ya lüğət — onu append, insert, dict[key, default:] += 1 və sair metodlarla yeniləmək daha təbii olur. Məhz bu “strukturun yığılması”dır. reduce(into:) də bunun üçün düşünüb hazırlanıb.

2. reduce(into:) necə işləyir

Adi reduce “yeni akkumulyatoru qaytar” kimi oxunursa, reduce(into:) demək olar ki, dürüst bir for dövrü kimi oxunur: nəticə üçün bir dəyişəniniz var və siz onu hər iterasiyada yeniləyirsiniz. Sadəcə burada bu, ifadə kimi rəsmiləşdirilib, akkumulyator isə lokal qalır — onu kənarda elan etməyə ehtiyac yoxdur və o, qonşu koda “sızmır”.

Sadələşdirilmiş şəkildə standart kitabxana bunu belə düşünür: siz başlanğıc nəticə initial verdiniz, sonra hər element üçün bağlanma çağırıldı, orada akkumulyatora inout girişi aldınız, sonra isə həmin akkumulyator kənara qaytarıldı. Fikri mümkün qədər birbaşa yenidən yazsaq, təxminən belə “saxta reallaşdırma” alınır (konseptual olaraq):

extension Array {
    func myReduceInto<Result>(
        _ initial: Result,
        _ update: (inout Result, Element) -> Void
    ) -> Result {
        var result = initial
        for element in self {
            update(&result, element)
        }
        return result
    }
}

Vacib məqam: update daxilində siz yeni nəticə qaytarmırsınız. Siz cari result-u “inout vasitəsilə”, yəni birbaşa, metodlarla və ya mənimsətmələrlə dəyişirsiniz. Bu, massivlərin və lüğətlərin “yığılması” üçün çox uyğun üslubdur.

Yadda saxlamaq üçün qısa sxem:

flowchart TD
    A["initialResult (empty array/dictionary/...)"] --> B["get the next element"]
    B --> C["update(&accumulator, element)"]
    C --> D{"any more elements?"}
    D -- yes --> B
    D -- no --> E["return accumulator"]

3. Tiplər və başlanğıc dəyər

İdeal dünyada biz reduce(into: []) yazardıq və kompilyator özü bunun məsələn, [Int] olduğunu anlayardı. Bəzən bu, həqiqətən də belə olur — xüsusilə də yaxın kontekst tipi açıq şəkildə müəyyən edirsə. Amma real həyatda siz tez-tez “Type of expression is ambiguous…” xətası alacaqsınız, çünki boş [] literalı təkbaşına hansı tip massiv toplamaq istədiyinizi demir.

Burada sehr yoxdur: kompilyator telepat deyil, sadəcə faktlara görə tipi çıxarmağa çalışır. Fakt azdırsa, sizdən kömək istəyir. Ən sadə üsul — boş strukturu açıq tip ilə yazmaqdır: [Int]() və ya [String: Int]().

Müqayisə edin:

import Foundation

let numbers = [1, 2, 3]

let doubled = numbers.reduce(into: [Int]()) { result, x in
    result.append(x * 2)
}

print(doubled) // [2, 4, 6]

Əgər solda tipi əvvəlcədən “göstərmisinizsə”, bunu da belə etmək olar:

import Foundation

let numbers = [1, 2, 3]

let doubled: [Int] = numbers.reduce(into: []) { result, x in
    result.append(x * 2)
}

print(doubled) // [2, 4, 6]

Hər iki variant normaldır. Praktikada başlanğıc səviyyəsində olanlara reduce(into: [Int]()) üslubuna öyrəşmək daha rahat olur: o, daha birbaşa görünür və kompilyator üçün nadir hallarda sual yaradır.

4. Nümunələr: massivlər, lüğətlər, qruplaşdırmalar

Şərt ilə bir keçiddə massiv yığılması

map və filter öyrənəndə biz belə bir ideyaya alışmışdıq: “hər metod bir addım edir”. Bu yaxşı üslubdur. Amma bəzən nəticəni bir keçiddə toplamaq faydalı olur: məsələn, kvadratı hesablayıb, şərti yoxlayıb, şərt keçərsə dəyəri yekun massivə əlavə etmək istəyirik. reduce(into:) bunu xarici dəyişən olmadan və massivlərin əlavə surətlərini yaratmadan etməyə imkan verir.

Sadədən başlayaq: yalnız 10-dan böyük kvadratları saxlayaq.

import Foundation

let numbers = [1, 2, 3, 4, 5]

let squaresOver10 = numbers.reduce(into: [Int]()) { result, x in
    let sq = x * x
    if sq > 10 {
        result.append(sq)
    }
}

print(squaresOver10) // [16, 25]

Burada üslubu başa düşmək vacibdir: bu, “reduce sehridir” deyil, sadəcə səliqəli qablaşdırılmış dövrdür. Biz result-u qaytarmırıq, çünki o, elə bağlanmanın daxilində yenilənir.

Bununla belə, dürüst deyək: əgər tapşırıq “çevirmək və filtrləmək”dirsə, onda filter → map daha oxunaqlı ola bilər. Əgər tapşırıq “şərt və əlavə məntiq ilə struktur yığmaq”dırsa, reduce(into:) çox vaxt daha aydın olur.

Sayıcı lüğət

Sayğac lüğəti (frequency map) proqramçının demək olar ki, isveçrə bıçağıdır: sözlərin, səslərin, xətaların, təkrarların, məşhur müəlliflərin və ümumiyyətlə, istənilən şeyin sayılması üçün. reduce(into:) isə bu paterni xüsusilə rahat edir, çünki siz lüğəti birbaşa mutasiya edirsiniz.

Klassik nümunə: hər sözün neçə dəfə göründüyünü sayaq.

import Foundation

let words = ["swift", "is", "swift", "fast"]

let counts = words.reduce(into: [String: Int]()) { result, word in
    result[word, default: 0] += 1
}

print(counts) // ["is": 1, "swift": 2, "fast": 1]

Bu niyə reduce(into:) ilə bu qədər yaxşı uyğunlaşır? Çünki lüğət “dəyişdirilə bilən struktur”dur və onu metodlarla/sabskriptlə dəyişmək məntiqlidir.

“Açar → dəyərlər massivi” sxemi üzrə qruplaşdırma

Qruplaşdırma — “açar → elementlər siyahısı” tipli struktur almaq istədiyimiz zamandır. Məsələn, sözləri uzunluğa görə qruplaşdırmaq. Belə kod for daxilində çox vaxt ağır görünür, amma reduce(into:) ilə çox rahat oxunur.

import Foundation

let words = ["hi", "swift", "ok", "code"]

let grouped = words.reduce(into: [Int: [String]]()) { result, word in
    result[word.count, default: []].append(word)
}

print(grouped) // [2: ["hi", "ok"], 5: ["swift"], 4: ["code"]]

Burada yadda saxlamağa dəyən iki şey var. Birincisi, default: [] açar hələ yoxdursa, boş massiv yaratmaq üçün rahat və təhlükəsiz yoldur. İkincisi, append tam hüquqlu və birbaşa olur: biz həqiqətən dəyərləri açar üzrə massivə “yığırıq”.

5. Mini-layihə: “mini-kitabxana” üzrə statistika

Mücərrəd “alma və portağal” söhbətində ilişib qalmamaq üçün təsəvvür edək ki, “mini-kitabxana” adlı konsol tətbiqi inkişaf etdiririk. Hələ siniflər və mürəkkəb arxitektura yoxdur: sadəcə hər sətiri "Başlıq|Müəllif|Teqlər" formatında olan sətirlər toplusu. Biz artıq split(separator:), Optional və compactMap biliriksə, sadə analitika toplamaq üçün bu kifayətdir.

Kitab sətrini tuple-a çevirmək

Kiçik bir parsing funksiyası yazaq. Əgər sətir qüsurludursa, məsələn, "|" olmadan verilirsə, o nil qaytaracaq. Öz struct-larımızı hələ yaratmırıq (bu, kursun sonrakı hissəsində olacaq), ona görə tuple istifadə edirik.

import Foundation

func parseBookLine(_ line: String) -> (title: String, author: String, tags: [String])? {
    let parts = line.split(separator: "|", omittingEmptySubsequences: false)
    guard parts.count >= 2 else { return nil }

    let title = String(parts[0])
    let author = String(parts[1])
    let tags = parts.count >= 3 ? parts[2].split(separator: ",").map(String.init) : []
    return (title, author, tags)
}

Burada hər şey tamamilə praktikdir: guard bizi “qüsurlu format”dan qoruyur, teqləri isə vergüllərə görə bölürük. Bəli, teqlər hələ trimming və normallaşdırma olmadan qalır — bu, ayrıca mövzudur və bu gün ona girmirik.

Məlumatları hazırlayırıq: compactMap + parser

İndi test məlumatları yaradaq və onları “kitablar” massivinə (tuples) çevirək. Burada compactMap çox uyğundur: o, nil-ləri, yəni pis sətirləri atacaq.

import Foundation

let rawLines = [
    "Clean Code|Robert Martin|programming,craft",
    "The Pragmatic Programmer|Andrew Hunt|programming",
    "Swift Programming|Apple|swift,programming",
    "BadLineWithoutSeparator"
]

let books = rawLines.compactMap(parseBookLine)
print(books.count) // 3

Müəlliflər üçün tezlik xəritəsi

İndi bizim “mini-kitabxana” ən çox hansı müəlliflərin olduğunu deyə bilir.

import Foundation

let authorCounts = books.reduce(into: [String: Int]()) { result, book in
    result[book.author, default: 0] += 1
}

print(authorCounts) // ["Robert Martin": 1, "Andrew Hunt": 1, "Apple": 1]

Teqlər üzrə indeks: tag → kitabların sayı

Burada artıq maraqlı olur: bir kitabda çoxlu teq var, deməli, iç-içə keçidə ehtiyacımız var. Qorxmuruq: reduce(into:) daxilində qısa və məqsədli bir iç for tamamilə normaldır.

import Foundation

let tagCounts = books.reduce(into: [String: Int]()) { result, book in
    for tag in book.tags {
        result[tag, default: 0] += 1
    }
}

print(tagCounts) // ["programming": 3, "craft": 1, "swift": 1]

Üsluba diqqət yetirin: biz “yeni lüğəti hiylə ilə qaytarmağa” çalışmırıq. Sadəcə sayğacı dürüst şəkildə artırırıq.

Kitabları başlığın ilk hərfinə görə qruplaşdırmaq

Daha bir kiçik indeks: başlığın ilk hərfi → başlıqların siyahısı. Bu artıq “kataloq”a bənzəyir.

import Foundation

let titlesByFirstLetter = books.reduce(into: [Character: [String]]()) { result, book in
    guard let first = book.title.first else { return }
    result[first, default: []].append(book.title)
}

print(titlesByFirstLetter["C"] ?? []) // ["Clean Code"]

Burada kiçik bir Optional guşəsi ilə rastlaşırıq: book.title.first Character? qaytarır, çünki sətir boş ola bilər. Biz səliqəli şəkildə guard tətbiq edirik və hər şey təhlükəsiz olur.

6. Performans və artıq surətlər

Mümkün qədər səmimi və şamanlıq olmadan desək: reduce(into:) rahatdır, çünki o, hər addımda yeni akkumulyator yaratmağa məcbur etmir. Adi reduce isə bunu tələb edir: siz bağlanmadan akkumulyatorun yeni dəyərini qaytarmalısınız. Bəzən bu “yeni dəyər” ucuz ola bilər (rəqəm), bəzən isə baha (böyük massiv).

Klassik tələ: adi reduce içində result + [element] vasitəsilə massiv toplamaq. Bu, gözəl görünür, amma potensial olaraq hər addımda yeni massiv yaranmasına və elementlərin kopyalanmasına səbəb olur. Belə yanaşma asanlıqla O(n²)-ə çevrilə bilər, halbuki append ilə akkumulyator mutasiyası O(n) olaraq qalır.

Bu o demək deyil ki, kodunuz “mütləq yavaş olacaq” — optimizator bəzən möcüzələr yaradır. Amma yeni başlayan üçün praktik qayda sadədir: əgər konteyner yığırsınızsa, reduce(into:) və ya adi for istifadə edin. Beləliklə, kod dərslikdən çıxmış kimi görünüb, amma hər 0.1 saniyədə iş masasına qovluq köçürmək kimi işləyən vəziyyətə düşməyəcəksiniz.

7. Nə vaxt reduce(into:), nə vaxt for seçməli

reduce(into:)-ya aşiq olub onunla hər şeyi yığmağa başlamaq çox asandır — təxminən ternar operatoru öyrənib sonra onunla roman yazan insanlar kimi. Bəli, texniki olaraq mümkündür. Xeyr, oxumaq asan olmayacaq.

reduce(into:) xüsusilə o zaman yaxşıdır ki, sizdə nəticənin aydın “yığılması” var və bu 3–8 sətirə sığır: massivə əlavə etmək, lüğətdə sayğacı artırmaq, elementləri qruplaşdırmaq. O, həm də akkumulyatorun lokal qalmasını və kənarda yaşamamasını istədiyiniz hallarda yaxşıdır (və bununla da fərqli hesablamalar arasında təsadüfən təkrar istifadə olunmaz).

Amma əgər reduce(into:) daxilində çoxlu budaqlanmalar, bir neçə iç-içə if səviyyəsi, bir neçə müvəqqəti dəyişən və “mən artıq harada olduğumu anlamıram” hissi yaranırsa, onda dürüst for daha oxunaqlı ola bilər. Dövr ayıb deyil və “başlanğıc səviyyəli kod” da deyil. Dövr axının idarə olunması üçün normal, aydın konstruksiyadır və bəzən fikri daha yaxşı ifadə edir.

8. reduce(into:) ilə işləyərkən tipik səhvlər

Səhv №1: akkumulyatoru adi reduce-dəki kimi return etməyə çalışmaq.
reduce(into:)-da bağlanma adətən Void qaytarır, yəni “heç nə”. Əgər vərdişə görə return result yazırsınızsa, ya kompilyasiya xətası alacaqsınız, ya da qəribə fəndlər etməyə başlayacaqsınız. Burada məntiq fərqlidir: siz result-u birbaşa append, mənimsətmələr və lüğətlə iş vasitəsilə mutasiya edirsiniz.

Səhv №2: reduce(into: []) yazıb tip çıxarılması xətasına düşmək.
Kontekstsiz boş [] literalı kompilyatora bunun hansı massiv olduğunu demir. Belə yerlərdə [Int]() və ya [String: Int]() yazmaq daha asan və rahatdır — bu, qeyri-müəyyənliyi aradan qaldırır və kodu özünüizahlı edir.

Səhv №3: adi reduce içində massivi result + [x] ilə yığmaq.
Bu, “funksional olaraq gözəl” görünür, amma çoxlu surətlərə və performansın pisləşməsinə gətirə bilər. Məhz belə hallara görə reduce(into:) meydana çıxıb: akkumulyatoru mutasiya etmək və kolleksiyanı hər addımda yenidən yaratmamaq üçün.

Səhv №4: reduce(into:)-nu bir sətirlik mini-proqrama çevirmək.
Bağlanmanın içində böyük if/else-lər, iç-içə dövrlər, mürəkkəb hesablamalar və “sehrli” indekslər olduqda oxunuş bir axtarışa çevrilir. Belə hallarda məntiqin bir hissəsini ayrıca, adı aydın olan funksiyaya çıxarmaq və ya axının idarəsi daha sadə oxunan for-a keçmək daha yaxşıdır.

Səhv №5: “çevirmə” və “yığma”nı qarışdırmaq.
Əgər sadəcə hər elementi 2-yə vurmaq istəyirsinizsə, map demək olar ki, həmişə reduce(into:)-dan daha oxunaqlıdır. reduce(into:) map/filter üçün “universal əvəz” deyil, məhz struktur yığdığınız və ya mutasiya ilə toplama apardığınız hallar üçün alətdir.

1
Tapşırıq
Swift SELF, səviyyə, dərs
Bağlanıb
İkiqat bonus
İkiqat bonus
1
Tapşırıq
Swift SELF, səviyyə, dərs
Bağlanıb
Söz sayıcı
Söz sayıcı
1
Tapşırıq
Swift SELF, səviyyə, dərs
Bağlanıb
Səbətlər üzrə təsnifat
Səbətlər üzrə təsnifat
1
Tapşırıq
Swift SELF, səviyyə, dərs
Bağlanıb
Kitab cədvəli
Kitab cədvəli
Şərhlər
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION